All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.mitchellbosecke.pebble.parser.ExpressionParser Maven / Gradle / Ivy

/*******************************************************************************
 * This file is part of Pebble.
 * 
 * Copyright (c) 2014 by Mitchell Bösecke
 * 
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 ******************************************************************************/
package com.mitchellbosecke.pebble.parser;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import com.mitchellbosecke.pebble.error.ParserException;
import com.mitchellbosecke.pebble.lexer.Token;
import com.mitchellbosecke.pebble.lexer.TokenStream;
import com.mitchellbosecke.pebble.lexer.Token.Type;
import com.mitchellbosecke.pebble.node.ArgumentsNode;
import com.mitchellbosecke.pebble.node.FunctionOrMacroNameNode;
import com.mitchellbosecke.pebble.node.NamedArgumentNode;
import com.mitchellbosecke.pebble.node.PositionalArgumentNode;
import com.mitchellbosecke.pebble.node.TestInvocationExpression;
import com.mitchellbosecke.pebble.node.expression.BinaryExpression;
import com.mitchellbosecke.pebble.node.expression.BlockFunctionExpression;
import com.mitchellbosecke.pebble.node.expression.ContextVariableExpression;
import com.mitchellbosecke.pebble.node.expression.Expression;
import com.mitchellbosecke.pebble.node.expression.FilterExpression;
import com.mitchellbosecke.pebble.node.expression.FilterInvocationExpression;
import com.mitchellbosecke.pebble.node.expression.FunctionOrMacroInvocationExpression;
import com.mitchellbosecke.pebble.node.expression.GetAttributeExpression;
import com.mitchellbosecke.pebble.node.expression.LiteralBooleanExpression;
import com.mitchellbosecke.pebble.node.expression.LiteralDoubleExpression;
import com.mitchellbosecke.pebble.node.expression.LiteralLongExpression;
import com.mitchellbosecke.pebble.node.expression.LiteralNullExpression;
import com.mitchellbosecke.pebble.node.expression.LiteralStringExpression;
import com.mitchellbosecke.pebble.node.expression.NegativeTestExpression;
import com.mitchellbosecke.pebble.node.expression.ParentFunctionExpression;
import com.mitchellbosecke.pebble.node.expression.PositiveTestExpression;
import com.mitchellbosecke.pebble.node.expression.TernaryExpression;
import com.mitchellbosecke.pebble.node.expression.UnaryExpression;
import com.mitchellbosecke.pebble.operator.Associativity;
import com.mitchellbosecke.pebble.operator.BinaryOperator;
import com.mitchellbosecke.pebble.operator.UnaryOperator;

/**
 * Parses expressions.
 */
public class ExpressionParser {

    private final Parser parser;

    private TokenStream stream;

    private Map binaryOperators;

    private Map unaryOperators;

    /**
     * Constructor
     * 
     * @param parser
     *            A reference to the main parser
     */
    public ExpressionParser(Parser parser, Map binaryOperators,
            Map unaryOperators) {
        this.parser = parser;
        this.binaryOperators = binaryOperators;
        this.unaryOperators = unaryOperators;
    }

    /**
     * The public entry point for parsing an expression.
     * 
     * @return NodeExpression the expression that has been parsed.
     * @throws ParserException
     */
    public Expression parseExpression() throws ParserException {
        return parseExpression(0);
    }

    /**
     * A private entry point for parsing an expression. This method takes in the
     * precedence required to operate a "precedence climbing" parsing algorithm.
     * It is a recursive method.
     * 
     * @see http://en.wikipedia.org/wiki/Operator-precedence_parser
     * 
     * @return The NodeExpression representing the parsed expression.
     * @throws ParserException
     */
    private Expression parseExpression(int minPrecedence) throws ParserException {

        this.stream = parser.getStream();
        Token token = stream.current();
        Expression expression = null;

        /*
         * The first check is to see if the expression begins with a unary
         * operator, or an opening bracket, or neither.
         */
        if (isUnary(token)) {
            UnaryOperator operator = this.unaryOperators.get(token.getValue());
            stream.next();
            expression = parseExpression(operator.getPrecedence());

            UnaryExpression unaryExpression = null;
            Class operatorNodeClass = operator.getNodeClass();
            try {
                unaryExpression = operatorNodeClass.newInstance();
            } catch (InstantiationException | IllegalAccessException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            unaryExpression.setChildExpression(expression);

            expression = unaryExpression;

        } else if (token.test(Token.Type.PUNCTUATION, "(")) {

            stream.next();
            expression = parseExpression();
            stream.expect(Token.Type.PUNCTUATION, ")");
            expression = parsePostfixExpression(expression);

        } else {
            /*
             * starts with neither. Let's parse out the first expression that we
             * can find. There may be one, there may be many (separated by
             * binary operators); right now we are just looking for the first.
             */
            expression = subparseExpression();
        }

        /*
         * If, after parsing the first expression we encounter a binary operator
         * then we know we have another expression on the other side of the
         * operator that requires parsing. Otherwise we're done.
         */
        token = stream.current();
        while (isBinary(token) && binaryOperators.get(token.getValue()).getPrecedence() >= minPrecedence) {

            // find out which operator we are dealing with and then skip over it
            BinaryOperator operator = binaryOperators.get(token.getValue());
            stream.next();

            Expression expressionRight = null;

            // the right hand expression of the FILTER operator is handled in a
            // unique way
            if (FilterExpression.class.equals(operator.getNodeClass())) {
                expressionRight = parseFilterInvocationExpression();
            }
            // the right hand expression of TEST operators is handled in a
            // unique way
            else if (PositiveTestExpression.class.equals(operator.getNodeClass())
                    || NegativeTestExpression.class.equals(operator.getNodeClass())) {
                expressionRight = parseTestInvocationExpression();
            } else {
                /*
                 * parse the expression on the right hand side of the operator
                 * while maintaining proper associativity and precedence
                 */
                expressionRight = parseExpression(Associativity.LEFT.equals(operator.getAssociativity()) ? operator
                        .getPrecedence() + 1 : operator.getPrecedence());
            }

            /*
             * we have to wrap the left and right side expressions into one
             * final expression. The operator provides us with the type of
             * expression we are creating.
             */
            BinaryExpression finalExpression = null;
            Class> operatorNodeClass = operator.getNodeClass();
            try {
                finalExpression = operatorNodeClass.newInstance();
            } catch (InstantiationException | IllegalAccessException e) {
                e.printStackTrace();
                throw new ParserException(e, "Error instantiating operator node [" + operatorNodeClass.getName() + "]",
                        token.getLineNumber(), stream.getFilename());
            }

            finalExpression.setLeft(expression);
            finalExpression.setRight(expressionRight);

            expression = finalExpression;

            token = stream.current();
        }

        if (minPrecedence == 0) {
            return parseTernaryExpression(expression);
        }

        return expression;
    }

    /**
     * Checks if a token is a unary operator.
     * 
     * @param token
     *            The token that we are checking
     * @return boolean Whether the token is a unary operator or not
     */
    private boolean isUnary(Token token) {
        return token.test(Token.Type.OPERATOR) && this.unaryOperators.containsKey(token.getValue());
    }

    /**
     * Checks if a token is a binary operator.
     * 
     * @param token
     *            The token that we are checking
     * @return boolean Whether the token is a binary operator or not
     */
    private boolean isBinary(Token token) {
        return token.test(Token.Type.OPERATOR) && this.binaryOperators.containsKey(token.getValue());
    }

    /**
     * Finds and returns the next "simple" expression; an expression of which
     * can be found on either side of a binary operator but does not contain a
     * binary operator. Ex. "var.field", "true", "12", etc.
     * 
     * @return NodeExpression The expression that it found.
     * @throws ParserException
     */
    private Expression subparseExpression() throws ParserException {
        final Token token = stream.current();
        Expression node = null;

        switch (token.getType()) {

        case NAME:
            switch (token.getValue()) {

            // a constant?
            case "true":
            case "TRUE":
                node = new LiteralBooleanExpression(true);
                break;
            case "false":
            case "FALSE":
                node = new LiteralBooleanExpression(false);
                break;
            case "none":
            case "NONE":
            case "null":
            case "NULL":
                node = new LiteralNullExpression();
                break;

            default:

                // name of a function?
                if (stream.peek().test(Token.Type.PUNCTUATION, "(")) {
                    node = new FunctionOrMacroNameNode(token.getValue());
                }

                // variable name
                else {
                    node = new ContextVariableExpression(token.getValue());
                }
                break;
            }
            break;

        case NUMBER:
            final String numberValue = token.getValue();
            if (numberValue.contains(".")) {
                node = new LiteralDoubleExpression(Double.valueOf(numberValue));
            } else {
                node = new LiteralLongExpression(Long.valueOf(numberValue));
            }

            break;

        case STRING:
            node = new LiteralStringExpression(token.getValue());
            break;

        // not found, syntax error
        default:
            throw new ParserException(null, String.format("Unexpected token \"%s\" of value \"%s\"", token.getType()
                    .toString(), token.getValue()), token.getLineNumber(), stream.getFilename());
        }

        // there may or may not be more to this expression - let's keep looking
        stream.next();
        return parsePostfixExpression(node);
    }

    @SuppressWarnings("unchecked")
    private Expression parseTernaryExpression(Expression expression) throws ParserException {
        while (this.stream.current().test(Token.Type.PUNCTUATION, "?")) {

            stream.next();

            Expression expression2 = null;
            Expression expression3 = null;

            if (!stream.current().test(Token.Type.PUNCTUATION, ":")) {
                expression2 = parseExpression();

                if (stream.current().test(Token.Type.PUNCTUATION, ":")) {
                    stream.next();
                    expression3 = parseExpression();
                }
            } else {
                stream.next();
                expression2 = expression;
                expression3 = parseExpression();
            }

            expression = new TernaryExpression((Expression) expression, expression2, expression3);
        }

        return expression;
    }

    /**
     * Determines if there is more to the provided expression than we originally
     * thought. We will look for the filter operator or perhaps we are getting
     * an attribute from a variable (ex. var.attribute or var['attribute'] or
     * var.attribute(bar)).
     * 
     * @param node
     *            The expression that we have already discovered
     * @return Either the original expression that was passed in or a slightly
     *         modified version of it, depending on what was discovered.
     * @throws ParserException
     */
    private Expression parsePostfixExpression(Expression node) throws ParserException {
        Token current;
        while (true) {
            current = stream.current();

            if (current.test(Token.Type.PUNCTUATION, ".") || current.test(Token.Type.PUNCTUATION, "[")) {

                // a period represents getting an attribute from a variable or
                // calling a method
                node = parseBeanAttributeExpression(node);

            } else if (current.test(Token.Type.PUNCTUATION, "(")) {

                // function call
                node = parseFunctionOrMacroInvocation(node);

            } else {
                break;
            }
        }
        return node;
    }

    private Expression parseFunctionOrMacroInvocation(Expression node) throws ParserException {
        String functionName = ((FunctionOrMacroNameNode) node).getName();
        ArgumentsNode args = parseArguments();

        /*
         * The following core functions have their own Nodes and are rendered in
         * unique ways for the sake of performance.
         */
        switch (functionName) {
        case "parent":
            return new ParentFunctionExpression(parser.peekBlockStack(), stream.current().getLineNumber());
        case "block":
            return new BlockFunctionExpression(args);
        }

        return new FunctionOrMacroInvocationExpression(functionName, args);
    }

    public FilterInvocationExpression parseFilterInvocationExpression() throws ParserException {
        TokenStream stream = parser.getStream();
        Token filterToken = stream.expect(Token.Type.NAME);

        ArgumentsNode args = null;
        if (stream.current().test(Token.Type.PUNCTUATION, "(")) {
            args = this.parseArguments();
        } else {
            args = new ArgumentsNode(null, null);
        }

        return new FilterInvocationExpression(filterToken.getValue(), args);
    }

    private Expression parseTestInvocationExpression() throws ParserException {
        TokenStream stream = parser.getStream();
        int lineNumber = stream.current().getLineNumber();

        Token testToken = stream.expect(Token.Type.NAME);

        ArgumentsNode args = null;
        if (stream.current().test(Token.Type.PUNCTUATION, "(")) {
            args = this.parseArguments();
        } else {
            args = new ArgumentsNode(null, null);
        }

        return new TestInvocationExpression(lineNumber, testToken.getValue(), args);
    }

    /**
     * A bean attribute expression can either be an expression getting an
     * attribute from a variable in the context, or calling a method from a
     * variable.
     * 
     * Ex. foo.bar or foo['bar'] or foo.bar('baz')
     * 
     * @param node
     *            The expression parsed so far
     * @return NodeExpression The parsed subscript expression
     * @throws ParserException
     */
    private Expression parseBeanAttributeExpression(Expression node) throws ParserException {
        TokenStream stream = parser.getStream();

        if (stream.current().test(Token.Type.PUNCTUATION, ".")) {

            // skip over the '.' token
            stream.next();

            Token token = stream.expect(Token.Type.NAME);

            ArgumentsNode args = null;
            if (stream.current().test(Token.Type.PUNCTUATION, "(")) {
                args = this.parseArguments();
                if (!args.getNamedArgs().isEmpty()) {
                    throw new ParserException(null, "Can not use named arguments when calling a bean method", stream
                            .current().getLineNumber(), stream.getFilename());
                }
            }

            node = new GetAttributeExpression(node, token.getValue(), args);

        } else if (stream.current().test(Token.Type.PUNCTUATION, "[")) {
            // skip over opening '[' bracket
            stream.next();

            // treat the string value inside the brackets just the same as we
            // would an attribute name following a '.', except that the
            // attribute name gathered this way is NOT held to the same naming
            // restrictions (e.g. can include hyphens '-')
            Token token = stream.current();
            if (token.test(Type.STRING) || token.test(Type.NUMBER)) {
                node = new GetAttributeExpression(node, token.getValue());
            } else {
                throw new ParserException(null, "Only strings and numbers allowed within square brackets.",
                        token.getLineNumber(), stream.getFilename());
            }
            stream.next();

            // move past the closing ']' bracket
            stream.expect(Token.Type.PUNCTUATION, "]");
        }

        return node;
    }

    public ArgumentsNode parseArguments() throws ParserException {
        return parseArguments(false);
    }

    public ArgumentsNode parseArguments(boolean isMacroDefinition) throws ParserException {

        List positionalArgs = new ArrayList<>();
        List namedArgs = new ArrayList<>();
        this.stream = this.parser.getStream();

        stream.expect(Token.Type.PUNCTUATION, "(");

        while (!stream.current().test(Token.Type.PUNCTUATION, ")")) {

            String argumentName = null;
            Expression argumentValue = null;

            if (!namedArgs.isEmpty() || !positionalArgs.isEmpty()) {
                stream.expect(Token.Type.PUNCTUATION, ",");
            }

            /*
             * Most arguments consist of VALUES with optional NAMES but in the
             * case of a macro definition the user is specifying NAMES with
             * optional VALUES. Therefore the logic changes slightly.
             */
            if (isMacroDefinition) {
                argumentName = parseNewVariableName();
                if (stream.current().test(Token.Type.PUNCTUATION, "=")) {
                    stream.expect(Token.Type.PUNCTUATION, "=");
                    argumentValue = parseExpression();
                }
            } else {
                if (stream.peek().test(Token.Type.PUNCTUATION, "=")) {
                    argumentName = parseNewVariableName();
                    stream.expect(Token.Type.PUNCTUATION, "=");
                }
                argumentValue = parseExpression();
            }

            if (argumentName == null) {
                if (!namedArgs.isEmpty()) {
                    throw new ParserException(null,
                            "Positional arguments must be declared before any named arguments.", stream.current()
                                    .getLineNumber(), stream.getFilename());
                }
                positionalArgs.add(new PositionalArgumentNode(argumentValue));
            } else {
                namedArgs.add(new NamedArgumentNode(argumentName, argumentValue));
            }

        }

        stream.expect(Token.Type.PUNCTUATION, ")");

        return new ArgumentsNode(positionalArgs, namedArgs);
    }

    /**
     * Parses a new variable that will need to be initialized in the Java code.
     * 
     * This is used for the set tag, the for loop, and in named arguments.
     * 
     * @return
     * @throws ParserException
     */
    public String parseNewVariableName() throws ParserException {

        // set the stream because this function may be called externally (for
        // and set token parsers)
        this.stream = this.parser.getStream();
        Token token = stream.current();
        token.test(Token.Type.NAME);

        String[] reserved = new String[] { "true", "false", "null", "none" };
        if (Arrays.asList(reserved).contains(token.getValue())) {
            throw new ParserException(null, String.format("Can not assign a value to %s", token.getValue()),
                    token.getLineNumber(), stream.getFilename());
        }

        stream.next();
        return token.getValue();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy