CompoundEvaluatedExpressionParser.java

/*
 * Copyright 2017-2022 original authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.micronaut.expressions.parser;

import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.expressions.parser.ast.ExpressionNode;
import io.micronaut.expressions.parser.ast.collection.OneDimensionalArray;
import io.micronaut.expressions.parser.ast.literal.StringLiteral;
import io.micronaut.expressions.parser.ast.operator.binary.AddOperator;
import io.micronaut.expressions.parser.ast.types.TypeIdentifier;
import io.micronaut.expressions.parser.exception.ExpressionParsingException;

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

import static io.micronaut.expressions.EvaluatedExpressionConstants.EXPRESSION_PREFIX;


/**
 * This parser is used to split complex expression into multiple
 * single expressions if necessary and delegate each atomic expression
 * parsing to separate instance of {@link SingleEvaluatedExpressionParser},
 * then combining single expressions parsing results.
 *
 * @author Sergey Gavrilov
 * @since 4.0.0
 */
@Internal
public final class CompoundEvaluatedExpressionParser implements EvaluatedExpressionParser {

    private final Object expression;

    /**
     * Instantiates compound expression parser.
     *
     * @param expression either string or string[]
     */
    public CompoundEvaluatedExpressionParser(@NonNull Object expression) {
        if (!(expression instanceof String || expression instanceof String[])) {
            throw new ExpressionParsingException("Can not parse expression: " + expression);
        }

        this.expression = expression;
    }

    @Override
    public ExpressionNode parse() throws ExpressionParsingException {
        // if expression doesn't have prefix, the whole string is treated as expression
        if (expression instanceof String str && !str.contains(EXPRESSION_PREFIX)) {
            return new SingleEvaluatedExpressionParser(str).parse();
        }

        return parseTemplateExpression(expression);
    }

    private ExpressionNode parseTemplateExpression(Object expression) {
        if (expression instanceof String str) {
            List<ExpressionNode> expressionParts =
                splitExpressionParts(str).stream()
                    .map(this::prepareExpressionPart)
                    .map(SingleEvaluatedExpressionParser::new)
                    .map(SingleEvaluatedExpressionParser::parse)
                    .toList();

            if (expressionParts.size() == 1) {
                return expressionParts.get(0);
            } else {
                return expressionParts.stream()
                           .reduce(new StringLiteral(""), AddOperator::new);
            }
        } else {
            List<ExpressionNode> arrayNodes =
                Arrays.stream((String[]) expression)
                    .map(this::parseTemplateExpression)
                    .toList();

            return buildArrayOfExpressions(arrayNodes);
        }
    }

    private ExpressionNode buildArrayOfExpressions(List<ExpressionNode> nodes) {
        TypeIdentifier arrayElementType = new TypeIdentifier("Object");
        return new OneDimensionalArray(arrayElementType, nodes);
    }

    private List<String> splitExpressionParts(String expression) {
        List<String> parts = new ArrayList<>();
        String nextPart = nextPart(expression);
        while (!nextPart.isEmpty()) {
            parts.add(nextPart);
            expression = expression.substring(nextPart.length());
            nextPart = nextPart(expression);
        }

        return parts;
    }

    private String nextPart(String expression) {
        if (expression.startsWith(EXPRESSION_PREFIX)) {
            int unbalancedParenthesis = 1;

            StringBuilder expressionPart = new StringBuilder(EXPRESSION_PREFIX);
            int pointer = EXPRESSION_PREFIX.length();
            while (unbalancedParenthesis > 0 && pointer < expression.length()) {
                char nextChar = expression.charAt(pointer++);
                expressionPart.append(nextChar);
                if (nextChar == '{') {
                    unbalancedParenthesis++;
                } else if (nextChar == '}') {
                    unbalancedParenthesis--;
                }
            }

            if (unbalancedParenthesis > 0) {
                throw new ExpressionParsingException("Unbalanced parenthesis in expression: " + expression);
            }

            return expressionPart.toString();
        } else {
            int substringUntil = expression.contains(EXPRESSION_PREFIX)
                                     ? expression.indexOf(EXPRESSION_PREFIX)
                                     : expression.length();
            return expression.substring(0, substringUntil);
        }
    }

    private String prepareExpressionPart(String expressionPart) {
        if (expressionPart.startsWith(EXPRESSION_PREFIX)) {
            return expressionPart.substring(EXPRESSION_PREFIX.length(),
                expressionPart.length() - 1);
        } else {
            return "'" + expressionPart + "'";
        }
    }
}