com.blazebit.persistence.parser.expression.JPQLSelectExpressionVisitorImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.blazebit.persistence.parser.expression.JPQLSelectExpressionVisitorImpl.java

Source

/*
 * Copyright 2014 - 2018 Blazebit.
 *
 * 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
 *
 *      http://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 com.blazebit.persistence.parser.expression;

import com.blazebit.persistence.parser.JPQLSelectExpressionBaseVisitor;
import com.blazebit.persistence.parser.JPQLSelectExpressionLexer;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.ArrayExpressionIntegerLiteralIndexContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.ArrayExpressionSingleElementPathIndexContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.ArrayExpressionStringLiteralIndexContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.Boolean_literalContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.ComparisonExpression_path_typeContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.ComparisonExpression_type_pathContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.DateLiteralContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.Escape_characterContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.ExtendedJoinPathExpressionContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.Functions_returning_datetimeContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.General_path_elementContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.IdentifierContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.In_itemContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.IndexFunctionContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.Macro_expressionContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.QuantifiedComparisonExpression_arithmeticContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.QuantifiedComparisonExpression_booleanContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.QuantifiedComparisonExpression_datetimeContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.QuantifiedComparisonExpression_entityContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.QuantifiedComparisonExpression_entitytypeContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.QuantifiedComparisonExpression_stringContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.SimpleJoinPathExpressionContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.SimplePathContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.SingleJoinElementExpressionContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.String_literalContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.TimeLiteralContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.TimestampLiteralContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.TreatJoinPathExpressionContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.TreatedRootPathContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.Treated_key_value_expressionContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.Treated_subpathContext;
import com.blazebit.persistence.parser.JPQLSelectExpressionParser.TrimFunctionContext;
import com.blazebit.persistence.parser.predicate.BetweenPredicate;
import com.blazebit.persistence.parser.predicate.BinaryExpressionPredicate;
import com.blazebit.persistence.parser.predicate.BooleanLiteral;
import com.blazebit.persistence.parser.predicate.CompoundPredicate;
import com.blazebit.persistence.parser.predicate.EqPredicate;
import com.blazebit.persistence.parser.predicate.ExistsPredicate;
import com.blazebit.persistence.parser.predicate.GePredicate;
import com.blazebit.persistence.parser.predicate.GtPredicate;
import com.blazebit.persistence.parser.predicate.InPredicate;
import com.blazebit.persistence.parser.predicate.IsEmptyPredicate;
import com.blazebit.persistence.parser.predicate.IsNullPredicate;
import com.blazebit.persistence.parser.predicate.LePredicate;
import com.blazebit.persistence.parser.predicate.LikePredicate;
import com.blazebit.persistence.parser.predicate.LtPredicate;
import com.blazebit.persistence.parser.predicate.MemberOfPredicate;
import com.blazebit.persistence.parser.predicate.Predicate;
import com.blazebit.persistence.parser.predicate.PredicateQuantifier;
import com.blazebit.persistence.parser.predicate.QuantifiableBinaryExpressionPredicate;
import com.blazebit.persistence.parser.util.TypeUtils;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.ErrorNode;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.antlr.v4.runtime.tree.TerminalNodeImpl;

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

/**
 *
 * @author Moritz Becker
 * @author Christian Beikov
 * @since 1.0.0
 */
public class JPQLSelectExpressionVisitorImpl extends JPQLSelectExpressionBaseVisitor<Expression> {

    private final Set<String> aggregateFunctions;
    private final Map<String, Class<Enum<?>>> enums;
    private final Map<String, Class<?>> entities;
    private final int minEnumSegmentCount;
    private final int minEntitySegmentCount;
    private final Map<String, MacroFunction> macros;
    private final Set<String> usedMacros;

    public JPQLSelectExpressionVisitorImpl(Set<String> aggregateFunctions, Map<String, Class<Enum<?>>> enums,
            Map<String, Class<?>> entities, int minEnumSegmentCount, int minEntitySegmentCount,
            Map<String, MacroFunction> macros, Set<String> usedMacros) {
        this.aggregateFunctions = aggregateFunctions;
        this.enums = enums;
        this.entities = entities;
        this.minEnumSegmentCount = minEnumSegmentCount;
        this.minEntitySegmentCount = minEntitySegmentCount;
        this.macros = macros;
        this.usedMacros = usedMacros;
    }

    @Override
    public Expression visitFunctions_returning_numerics_default(
            JPQLSelectExpressionParser.Functions_returning_numerics_defaultContext ctx) {
        return handleFunction(ctx.getStart().getText(), ctx);
    }

    @Override
    public Expression visitOuter_expression(JPQLSelectExpressionParser.Outer_expressionContext ctx) {
        return handleFunction(ctx.getStart().getText(), ctx);
    }

    @Override
    public Expression visitMacro_expression(Macro_expressionContext ctx) {
        final String macroName = ctx.macroName.getText().toUpperCase();
        return visitMacroExpression(macroName, ctx);
    }

    public Expression visitMacroExpression(String macroName, ParserRuleContext ctx) {
        List<Expression> funcArgs = new ArrayList<Expression>(ctx.getChildCount());
        // Special handling of empty invocation, the position 2 contains an empty child node
        if (ctx.getChildCount() != 4 || !ctx.getChild(2).getText().isEmpty()) {
            for (int i = 0; i < ctx.getChildCount(); i++) {
                if (!(ctx.getChild(i) instanceof TerminalNode)) {
                    funcArgs.add(ctx.getChild(i).accept(this));
                }
            }
        }

        MacroFunction macro = macros.get(macroName);
        if (macro == null) {
            throw new SyntaxErrorException("The macro '" + macroName + "' could not be found in the macro map!");
        }
        if (usedMacros != null) {
            usedMacros.add(macroName);
        }
        try {
            return macro.apply(funcArgs);
        } catch (RuntimeException ex) {
            throw new IllegalArgumentException("Could not apply the macro for the expression: " + ctx.getText(),
                    ex);
        }
    }

    @Override
    public Expression visitCoalesce_expression(JPQLSelectExpressionParser.Coalesce_expressionContext ctx) {
        return handleFunction(ctx.getStart().getText(), ctx);
    }

    @Override
    public Expression visitFunctions_returning_numerics_size(
            JPQLSelectExpressionParser.Functions_returning_numerics_sizeContext ctx) {
        FunctionExpression func = handleFunction(ctx.getStart().getText(), ctx);
        ((PathExpression) func.getExpressions().get(0)).setUsedInCollectionFunction(true);
        return func;
    }

    @Override
    public Expression visitFunctions_returning_datetime(Functions_returning_datetimeContext ctx) {
        return handleFunction(ctx.getStart().getText(), ctx);
    }

    @Override
    public Expression visitStringFunction(JPQLSelectExpressionParser.StringFunctionContext ctx) {
        return handleFunction(ctx.getStart().getText(), ctx);
    }

    @Override
    public Expression visitTrimFunction(TrimFunctionContext ctx) {
        Trimspec trimspec;

        if (ctx.trim_specification() != null) {
            trimspec = Trimspec.valueOf(ctx.trim_specification().getText().toUpperCase());
        } else {
            trimspec = Trimspec.BOTH;
        }

        Expression trimCharacter = null;

        if (ctx.trim_character() != null) {
            trimCharacter = ctx.trim_character().accept(this);
        }

        return new TrimExpression(trimspec, trimCharacter, ctx.string_expression().accept(this));
    }

    @Override
    public Expression visitAggregateExpression(JPQLSelectExpressionParser.AggregateExpressionContext ctx) {
        return new AggregateExpression(ctx.distinct != null, ctx.funcname.getText(),
                Arrays.asList((Expression) ctx.aggregate_argument().accept(this)));
    }

    @Override
    public Expression visitCountStar(JPQLSelectExpressionParser.CountStarContext ctx) {
        return new AggregateExpression();
    }

    @Override
    public Expression visitNullif_expression(JPQLSelectExpressionParser.Nullif_expressionContext ctx) {
        return handleFunction(ctx.getStart().getText(), ctx);
    }

    @Override
    public Expression visitNull_literal(JPQLSelectExpressionParser.Null_literalContext ctx) {
        return new NullExpression();
    }

    private FunctionExpression handleFunction(String name, ParseTree ctx) {
        List<Expression> funcArgs = new ArrayList<Expression>(ctx.getChildCount());
        for (int i = 0; i < ctx.getChildCount(); i++) {
            if (!(ctx.getChild(i) instanceof TerminalNode)) {
                funcArgs.add(ctx.getChild(i).accept(this));
            }
        }

        if ("FUNCTION".equalsIgnoreCase(name) && funcArgs.size() > 0
                && aggregateFunctions.contains(getLiteralString(funcArgs.get(0)).toLowerCase())) {
            return new AggregateExpression(false, name, funcArgs);
        } else {
            return new FunctionExpression(name, funcArgs);
        }
    }

    @Override
    public Expression visitFunction_invocation(JPQLSelectExpressionParser.Function_invocationContext ctx) {
        List<Expression> funcArgs = new ArrayList<Expression>(ctx.getChildCount());
        funcArgs.add(ctx.string_literal().accept(this));
        for (JPQLSelectExpressionParser.Function_argContext argCtx : ctx.args) {
            funcArgs.add(argCtx.accept(this));
        }

        String name = ctx.getStart().getText();
        if ("FUNCTION".equalsIgnoreCase(name) && funcArgs.size() > 0
                && aggregateFunctions.contains(getLiteralString(funcArgs.get(0)).toLowerCase())) {
            return new AggregateExpression(false, name, funcArgs);
        } else {
            return new FunctionExpression(name, funcArgs);
        }
    }

    private String getLiteralString(Expression expr) {
        String str = expr.toString();
        return str.substring(1, str.length() - 1);
    }

    @Override
    public Expression visitSimple_subpath(JPQLSelectExpressionParser.Simple_subpathContext ctx) {
        List<PathElementExpression> pathElements = new ArrayList<PathElementExpression>();
        pathElements.add((PathElementExpression) ctx.general_path_start().accept(this));
        for (JPQLSelectExpressionParser.General_path_elementContext generalPathElem : ctx.general_path_element()) {
            pathElements.add((PathElementExpression) generalPathElem.accept(this));
        }
        return new PathExpression(pathElements);
    }

    @Override
    public Expression visitTreated_subpath(Treated_subpathContext ctx) {
        TreatExpression treatExpression = new TreatExpression(ctx.general_subpath().accept(this),
                ctx.subtype().getText());
        List<General_path_elementContext> followingPaths = ctx.general_path_element();
        Expression finalExpression = treatExpression;

        if (followingPaths.size() > 0) {
            List<PathElementExpression> pathProperties = new ArrayList<PathElementExpression>(
                    followingPaths.size() + 1);
            PathExpression path = new PathExpression(pathProperties);
            pathProperties.add(treatExpression);

            for (int i = 0; i < followingPaths.size(); i++) {
                // TODO: Can here be arrays or is it just path elements?
                pathProperties.add((PathElementExpression) followingPaths.get(i).accept(this));
            }

            finalExpression = path;
        }

        return finalExpression;
    }

    @Override
    public Expression visitPath(JPQLSelectExpressionParser.PathContext ctx) {
        PathExpression result = wrapPath(ctx.general_subpath().accept(this));
        result.getExpressions().add((PathElementExpression) ctx.general_path_element().accept(this));

        if (result.getExpressions().size() >= minEnumSegmentCount) {
            for (PathElementExpression element : result.getExpressions()) {
                if (!(element instanceof PropertyExpression)) {
                    return result;
                }
            }

            String literalStr = ctx.getText();
            Expression literalExpression = createEnumLiteral(literalStr);
            if (literalExpression != null) {
                return literalExpression;
            }
        } else if (result.getExpressions().size() >= minEntitySegmentCount || result.getExpressions().size() == 1) {
            for (PathElementExpression element : result.getExpressions()) {
                if (!(element instanceof PropertyExpression)) {
                    return result;
                }
            }

            String literalStr = ctx.getText();
            Expression literalExpression = createEntityTypeLiteral(literalStr);
            if (literalExpression != null) {
                return literalExpression;
            }
        }

        return result;
    }

    @Override
    public Expression visitLiteral(JPQLSelectExpressionParser.LiteralContext ctx) {
        JPQLSelectExpressionParser.Simple_literalContext literalContext = ctx.simple_literal();
        if (literalContext != null) {
            return literalContext.accept(this);
        }
        String literalStr = ctx.getText();
        if (ctx.pathElem.size() > minEnumSegmentCount) {
            Expression literalExpression = createEnumLiteral(literalStr);
            if (literalExpression != null) {
                return literalExpression;
            }
        }

        Expression literalExpression = createEntityTypeLiteral(literalStr);
        if (literalExpression != null) {
            return literalExpression;
        }

        throw new SyntaxErrorException("Could not interpret '" + literalStr + "' as enum or entity literal!");
    }

    @Override
    public Expression visitEntity_type_or_literal_expression(
            JPQLSelectExpressionParser.Entity_type_or_literal_expressionContext ctx) {
        JPQLSelectExpressionParser.Entity_type_expressionContext context = ctx.entity_type_expression();
        if (context != null) {
            return context.accept(this);
        }

        String literalStr = ctx.getText();
        Expression literalExpression = createEntityTypeLiteral(literalStr);
        if (literalExpression != null) {
            return literalExpression;
        }

        throw new SyntaxErrorException("Could not interpret '" + literalStr + "' as entity literal!");
    }

    @Override
    public Expression visitSingle_element_path_expression(
            JPQLSelectExpressionParser.Single_element_path_expressionContext ctx) {
        Expression entityLiteral = createEntityTypeLiteral(ctx.general_path_start().getText());
        if (entityLiteral != null) {
            return entityLiteral;
        }

        List<PathElementExpression> pathElementExpressions = new ArrayList<>(1);
        pathElementExpressions.add((PathElementExpression) ctx.general_path_start().accept(this));
        return new PathExpression(pathElementExpressions);
    }

    @Override
    public Expression visitSimple_path_element(JPQLSelectExpressionParser.Simple_path_elementContext ctx) {
        return new PropertyExpression(ctx.identifier().getText());
    }

    @Override
    public Expression visitCollection_member_expression(
            JPQLSelectExpressionParser.Collection_member_expressionContext ctx) {
        PathExpression collectionPath = (PathExpression) ctx.collection_valued_path_expression().accept(this);
        collectionPath.setUsedInCollectionFunction(true);
        return new MemberOfPredicate(ctx.entity_or_value_expression().accept(this), collectionPath,
                ctx.not != null);
    }

    @Override
    public Expression visitEmpty_collection_comparison_expression(
            JPQLSelectExpressionParser.Empty_collection_comparison_expressionContext ctx) {
        PathExpression collectionPath = (PathExpression) ctx.collection_valued_path_expression().accept(this);
        collectionPath.setUsedInCollectionFunction(true);
        return new IsEmptyPredicate(collectionPath, ctx.not != null);
    }

    @Override
    public Expression visitType_discriminator(JPQLSelectExpressionParser.Type_discriminatorContext ctx) {
        return new TypeFunctionExpression(ctx.type_discriminator_arg().accept(this));
    }

    @Override
    public Expression visitEntryFunction(JPQLSelectExpressionParser.EntryFunctionContext ctx) {
        PathExpression collectionPath = (PathExpression) ctx.collection_valued_path_expression().accept(this);
        collectionPath.setCollectionKeyPath(true);
        return new MapEntryExpression(collectionPath);
    }

    @Override
    public Expression visitKey_value_expression(JPQLSelectExpressionParser.Key_value_expressionContext ctx) {
        PathExpression collectionPath = (PathExpression) ctx.collection_valued_path_expression().accept(this);
        collectionPath.setCollectionKeyPath(true);
        if ("VALUE".equalsIgnoreCase(ctx.name.getText())) {
            return new MapValueExpression(collectionPath);
        } else {
            return new MapKeyExpression(collectionPath);
        }
    }

    @Override
    public Expression visitTreated_key_value_expression(Treated_key_value_expressionContext ctx) {
        return new TreatExpression(ctx.key_value_expression().accept(this), ctx.subtype().getText());
    }

    @Override
    public Expression visitOuterJoinPathExpression(JPQLSelectExpressionParser.OuterJoinPathExpressionContext ctx) {
        return handleFunction(ctx.getStart().getText(), ctx);
    }

    @Override
    public Expression visitMacroJoinPathExpression(JPQLSelectExpressionParser.MacroJoinPathExpressionContext ctx) {
        final String macroName = ctx.macroName.getText().toUpperCase();
        return visitMacroExpression(macroName, ctx);
    }

    @Override
    public Expression visitSimpleJoinPathExpression(SimpleJoinPathExpressionContext ctx) {
        PathExpression path = (PathExpression) ctx.simple_subpath().accept(this);
        path.getExpressions().add((PathElementExpression) ctx.general_path_element().accept(this));
        return path;
    }

    @Override
    public Expression visitExtendedJoinPathExpression(ExtendedJoinPathExpressionContext ctx) {
        PathExpression path = wrapPath(ctx.treated_subpath().accept(this));
        path.getExpressions().add((PathElementExpression) ctx.general_path_element().accept(this));
        return path;
    }

    @Override
    public Expression visitSingleJoinElementExpression(SingleJoinElementExpressionContext ctx) {
        return ctx.single_element_path_expression().accept(this);
    }

    @Override
    public Expression visitTreatJoinPathExpression(TreatJoinPathExpressionContext ctx) {
        return new TreatExpression(ctx.join_path_expression().accept(this), ctx.subtype().getText());
    }

    @Override
    public Expression visitSimplePath(SimplePathContext ctx) {
        PathExpression path = (PathExpression) ctx.simple_subpath().accept(this);
        path.getExpressions().add((PathElementExpression) ctx.general_path_element().accept(this));
        return path;
    }

    @Override
    public Expression visitTreatedRootPath(TreatedRootPathContext ctx) {
        TreatExpression treatExpression = new TreatExpression(
                wrapPath(new PropertyExpression(ctx.identifier().getText())), ctx.subtype().getText());
        PathExpression path = (PathExpression) ctx.simple_subpath().accept(this);
        path.getExpressions().add(0, treatExpression);
        return path;
    }

    @Override
    public Expression visitIndexFunction(IndexFunctionContext ctx) {
        PathExpression collectionPath = (PathExpression) ctx.collection_valued_path_expression().accept(this);
        collectionPath.setCollectionKeyPath(true);
        return new ListIndexExpression(collectionPath);
    }

    @Override
    public Expression visitArrayExpressionParameterIndex(
            JPQLSelectExpressionParser.ArrayExpressionParameterIndexContext ctx) {
        return new ArrayExpression((PropertyExpression) ctx.simple_path_element().accept(this),
                ctx.Input_parameter().accept(this));
    }

    @Override
    public Expression visitArrayExpressionPathIndex(
            JPQLSelectExpressionParser.ArrayExpressionPathIndexContext ctx) {
        return new ArrayExpression((PropertyExpression) ctx.simple_path_element().accept(this),
                ctx.state_field_path_expression().accept(this));
    }

    @Override
    public Expression visitArrayExpressionSingleElementPathIndex(ArrayExpressionSingleElementPathIndexContext ctx) {
        return new ArrayExpression((PropertyExpression) ctx.simple_path_element().accept(this),
                ctx.single_element_path_expression().accept(this));
    }

    @Override
    public Expression visitArrayExpressionIntegerLiteralIndex(ArrayExpressionIntegerLiteralIndexContext ctx) {
        return new ArrayExpression((PropertyExpression) ctx.simple_path_element().accept(this),
                new NumericLiteral(ctx.Integer_literal().getText(), NumericType.INTEGER));
    }

    @Override
    public Expression visitArrayExpressionStringLiteralIndex(ArrayExpressionStringLiteralIndexContext ctx) {
        return new ArrayExpression((PropertyExpression) ctx.simple_path_element().accept(this),
                ctx.string_literal().accept(this));
    }

    @Override
    public Expression visitArithmeticExpressionPlusMinus(
            JPQLSelectExpressionParser.ArithmeticExpressionPlusMinusContext ctx) {
        ArithmeticOperator op = ArithmeticOperator.fromSymbol(ctx.op.getText());
        if (op == null) {
            throw new IllegalStateException("Unexpected arithmetic operator symbol [" + ctx.op.getText() + "]");
        }
        return new ArithmeticExpression(ctx.arithmetic_expression().accept(this),
                ctx.arithmetic_term().accept(this), op);
    }

    @Override
    public Expression visitArithmeticPrimaryParanthesis(
            JPQLSelectExpressionParser.ArithmeticPrimaryParanthesisContext ctx) {
        return ctx.arithmetic_expression().accept(this);
    }

    @Override
    public Expression visitArithmeticMultDiv(JPQLSelectExpressionParser.ArithmeticMultDivContext ctx) {
        ArithmeticOperator op = ArithmeticOperator.fromSymbol(ctx.op.getText());
        if (op == null) {
            throw new IllegalStateException("Unexpected arithmetic operator symbol [" + ctx.op.getText() + "]");
        }
        return new ArithmeticExpression(ctx.arithmetic_term().accept(this), ctx.arithmetic_factor().accept(this),
                op);
    }

    @Override
    public Expression visitBetweenArithmetic(JPQLSelectExpressionParser.BetweenArithmeticContext ctx) {
        return new BetweenPredicate(ctx.expr.accept(this), ctx.bound1.accept(this), ctx.bound2.accept(this),
                ctx.not != null);
    }

    @Override
    public Expression visitBetweenDatetime(JPQLSelectExpressionParser.BetweenDatetimeContext ctx) {
        return new BetweenPredicate(ctx.expr.accept(this), ctx.bound1.accept(this), ctx.bound2.accept(this),
                ctx.not != null);
    }

    @Override
    public Expression visitBetweenString(JPQLSelectExpressionParser.BetweenStringContext ctx) {
        return new BetweenPredicate(ctx.expr.accept(this), ctx.bound1.accept(this), ctx.bound2.accept(this),
                ctx.not != null);
    }

    @Override
    public Expression visitConditionalTerm_and(JPQLSelectExpressionParser.ConditionalTerm_andContext ctx) {
        Predicate left = (Predicate) ctx.conditional_term().accept(this);
        if (left instanceof CompoundPredicate
                && ((CompoundPredicate) left).getOperator() == CompoundPredicate.BooleanOperator.AND) {
            ((CompoundPredicate) left).getChildren().add((Predicate) ctx.conditional_factor().accept(this));
            return left;
        } else {
            return new CompoundPredicate(CompoundPredicate.BooleanOperator.AND, left,
                    (Predicate) ctx.conditional_factor().accept(this));
        }
    }

    @Override
    public Expression visitConditionalExpression_or(
            JPQLSelectExpressionParser.ConditionalExpression_orContext ctx) {
        Predicate left = (Predicate) ctx.conditional_expression().accept(this);
        if (left instanceof CompoundPredicate
                && ((CompoundPredicate) left).getOperator() == CompoundPredicate.BooleanOperator.OR) {
            ((CompoundPredicate) left).getChildren().add((Predicate) ctx.conditional_term().accept(this));
            return left;
        } else {
            return new CompoundPredicate(CompoundPredicate.BooleanOperator.OR, left,
                    (Predicate) ctx.conditional_term().accept(this));
        }
    }

    @Override
    public Expression visitConditionalPrimary(JPQLSelectExpressionParser.ConditionalPrimaryContext ctx) {
        return ctx.conditional_expression().accept(this);
    }

    @Override
    public Expression visitConditional_factor(JPQLSelectExpressionParser.Conditional_factorContext ctx) {
        Predicate predicate = (Predicate) ctx.conditional_primary().accept(this);

        if (ctx.not != null) {
            if (predicate.isNegated()) {
                // wrap in this case to maintain negational structure
                predicate = new CompoundPredicate(CompoundPredicate.BooleanOperator.AND, predicate);
            }
            predicate.negate();
        }
        return predicate;
    }

    @Override
    public Expression visitArithmetic_factor(JPQLSelectExpressionParser.Arithmetic_factorContext ctx) {
        if (ctx.signum != null) {
            Expression expression = ctx.arithmetic_primary().accept(this);

            boolean invertSignum = "-".equals(ctx.signum.getText());
            return new ArithmeticFactor(expression, invertSignum);
        } else {
            return ctx.arithmetic_primary().accept(this);
        }
    }

    @Override
    public Expression visitNumeric_literal(JPQLSelectExpressionParser.Numeric_literalContext ctx) {
        NumericType numericType;
        String value;
        if (ctx.BigInteger_literal() != null) {
            numericType = NumericType.BIG_INTEGER;
            value = ctx.BigInteger_literal().getText();
        } else if (ctx.Integer_literal() != null) {
            numericType = NumericType.INTEGER;
            value = ctx.Integer_literal().getText();
        } else if (ctx.Long_literal() != null) {
            numericType = NumericType.LONG;
            value = ctx.Long_literal().getText();
        } else if (ctx.Float_literal() != null) {
            numericType = NumericType.FLOAT;
            value = ctx.Float_literal().getText();
        } else if (ctx.Double_literal() != null) {
            numericType = NumericType.DOUBLE;
            value = ctx.Double_literal().getText();
        } else if (ctx.BigDecimal_literal() != null) {
            numericType = NumericType.BIG_DECIMAL;
            value = ctx.BigDecimal_literal().getText();
        } else {
            throw new IllegalStateException("Could not find literal in context [" + ctx.getText() + "]");
        }
        return new NumericLiteral(value, numericType);
    }

    @Override
    public Expression visitBoolean_literal(Boolean_literalContext ctx) {
        return new BooleanLiteral(Boolean.parseBoolean(ctx.Boolean_literal().getText()));
    }

    @Override
    public Expression visitString_literal(String_literalContext ctx) {
        String literalValue = ctx.String_literal() == null ? ctx.Character_literal().getText()
                : ctx.String_literal().getText();
        // strip quotes
        return new StringLiteral(unquote(literalValue));
    }

    @Override
    public Expression visitDateLiteral(DateLiteralContext ctx) {
        return new DateLiteral(
                TypeUtils.DATE_CONVERTER.convert(extractTemporalValueString(ctx.Date_literal().getText())));
    }

    @Override
    public Expression visitTimeLiteral(TimeLiteralContext ctx) {
        return new TimeLiteral(
                TypeUtils.TIME_CONVERTER.convert(extractTemporalValueString(ctx.Time_literal().getText())));
    }

    @Override
    public Expression visitTimestampLiteral(TimestampLiteralContext ctx) {
        return new TimestampLiteral(TypeUtils.TIMESTAMP_CONVERTER
                .convert(extractTemporalValueString(ctx.Timestamp_literal().getText())));
    }

    private String extractTemporalValueString(String input) {
        int start = input.indexOf('\'') + 1;
        int end = input.lastIndexOf('\'');
        return input.substring(start, end);
    }

    @Override
    public Expression visitNull_comparison_expression(
            JPQLSelectExpressionParser.Null_comparison_expressionContext ctx) {
        return new IsNullPredicate(ctx.getChild(0).accept(this), ctx.not != null);
    }

    @Override
    public Expression visitLike_expression(JPQLSelectExpressionParser.Like_expressionContext ctx) {
        // @formatter:off
        return new LikePredicate(ctx.string_expression().accept(this), ctx.pattern_value().accept(this), true,
                ctx.escape_character() != null ? ctx.escape_character().accept(this).toString().charAt(1) : null,
                ctx.not != null);
        // @formatter:on
    }

    @Override
    public Expression visitEscape_character(Escape_characterContext ctx) {
        if (ctx.Character_literal() != null) {
            return new StringLiteral(unquote(ctx.Character_literal().getText()));
        } else {
            return super.visitEscape_character(ctx);
        }
    }

    @Override
    public Expression visitGeneral_case_expression(JPQLSelectExpressionParser.General_case_expressionContext ctx) {
        List<WhenClauseExpression> whenClauses = new ArrayList<WhenClauseExpression>();
        for (JPQLSelectExpressionParser.When_clauseContext whenClause : ctx.when_clause()) {
            whenClauses.add((WhenClauseExpression) whenClause.accept(this));
        }
        JPQLSelectExpressionParser.Scalar_expressionContext elseExpressionCtx = ctx.scalar_expression();
        return new GeneralCaseExpression(whenClauses,
                elseExpressionCtx == null ? null : elseExpressionCtx.accept(this));
    }

    @Override
    public Expression visitSimple_case_expression(JPQLSelectExpressionParser.Simple_case_expressionContext ctx) {
        List<WhenClauseExpression> whenClauses = new ArrayList<WhenClauseExpression>();
        for (JPQLSelectExpressionParser.Simple_when_clauseContext whenClause : ctx.simple_when_clause()) {
            whenClauses.add((WhenClauseExpression) whenClause.accept(this));
        }
        JPQLSelectExpressionParser.Scalar_expressionContext elseExpressionCtx = ctx.scalar_expression();
        return new SimpleCaseExpression(ctx.case_operand().accept(this), whenClauses,
                elseExpressionCtx == null ? null : elseExpressionCtx.accept(this));
    }

    @Override
    public Expression visitWhen_clause(JPQLSelectExpressionParser.When_clauseContext ctx) {
        return handleWhenClause(ctx.conditional_expression(), ctx.scalar_expression());
    }

    @Override
    public Expression visitSimple_when_clause(JPQLSelectExpressionParser.Simple_when_clauseContext ctx) {
        return handleWhenClause(ctx.scalar_expression(0), ctx.scalar_expression(1));
    }

    private WhenClauseExpression handleWhenClause(ParserRuleContext condition, ParserRuleContext result) {
        return new WhenClauseExpression(condition.accept(this), result.accept(this));
    }

    @Override
    public Expression visitErrorNode(ErrorNode node) {
        throw new SyntaxErrorException("Parsing failed: " + node.getText());
    }

    @Override
    public Expression visitIn_expression(JPQLSelectExpressionParser.In_expressionContext ctx) {
        InPredicate inPredicate;
        Expression left;
        if (ctx.left == null) {
            left = ctx.getChild(0).accept(this);
        } else {
            List<PathElementExpression> pathElems = new ArrayList<>();
            pathElems.add(new PropertyExpression(ctx.left.getText()));
            left = new PathExpression(pathElems);
        }
        if (ctx.param == null && !ctx.in_item().isEmpty()) {
            List<Expression> inItems = new ArrayList<Expression>();
            for (In_itemContext inItemCtx : ctx.in_item()) {
                inItems.add(inItemCtx.accept(this));
            }
            inPredicate = new InPredicate(left, inItems);
        } else if (ctx.param != null) {
            ParameterExpression collectionParam = (ParameterExpression) new TerminalNodeImpl(ctx.param)
                    .accept(this);
            collectionParam.setCollectionValued(true);
            inPredicate = new InPredicate(left, collectionParam);
        } else {
            //            List<PathElementExpression> pathElems = new ArrayList<PathElementExpression>();
            //            pathElems.add(new PropertyExpression(ctx.right.getText()));
            //            Expression inExpr = new PathExpression(pathElems);
            Expression inExpr = ctx.getChild(ctx.not == null ? 2 : 3).accept(this);
            inPredicate = new InPredicate(left, inExpr);
        }
        if (ctx.not != null) {
            inPredicate.setNegated(true);
        }
        return inPredicate;
    }

    @Override
    public Expression visitTerminal(TerminalNode node) {
        if (node.getSymbol().getType() == JPQLSelectExpressionLexer.EOF) {
            return null;
        }
        switch (node.getSymbol().getType()) {
        case JPQLSelectExpressionLexer.Input_parameter:
            return new ParameterExpression(node.getText().substring(1));
        default:
            throw new IllegalStateException("Terminal node '" + node.getText() + "' not handled");
        }
    }

    @Override
    public Expression visitParseSimpleExpression(JPQLSelectExpressionParser.ParseSimpleExpressionContext ctx) {
        return super.visitParseSimpleExpression(ctx);
    }

    @Override
    public Expression visitParseSimpleSubqueryExpression(
            JPQLSelectExpressionParser.ParseSimpleSubqueryExpressionContext ctx) {
        return super.visitParseSimpleSubqueryExpression(ctx);
    }

    @Override
    public Expression visitParseOrderByClause(JPQLSelectExpressionParser.ParseOrderByClauseContext ctx) {
        return ctx.getChild(0).accept(this);
    }

    @Override
    public Expression visitComparisonExpression_path_type(ComparisonExpression_path_typeContext ctx) {
        BinaryExpressionPredicate pred = (EqPredicate) ctx.equality_comparison_operator().accept(this);
        String literalStr = ctx.left.getText();
        Expression expression = createEntityTypeLiteral(literalStr);
        if (expression == null) {
            throw new SyntaxErrorException("Could not interpret '" + literalStr + "' as entity literal!");
        }
        pred.setLeft(expression);
        pred.setRight(ctx.right.accept(this));
        return pred;
    }

    @Override
    public Expression visitComparisonExpression_type_path(ComparisonExpression_type_pathContext ctx) {
        BinaryExpressionPredicate pred = (EqPredicate) ctx.equality_comparison_operator().accept(this);
        pred.setLeft(ctx.left.accept(this));
        String literalStr = ctx.right.getText();
        Expression expression = createEntityTypeLiteral(literalStr);
        if (expression == null) {
            throw new SyntaxErrorException("Could not interpret '" + literalStr + "' as entity literal!");
        }
        pred.setRight(expression);
        return pred;
    }

    @Override
    public Expression visitComparisonExpression_string(
            JPQLSelectExpressionParser.ComparisonExpression_stringContext ctx) {
        return handleComparison(ctx.left, ctx.comparison_operator(), ctx.right);
    }

    @Override
    public Expression visitQuantifiedComparisonExpression_string(QuantifiedComparisonExpression_stringContext ctx) {
        return handleQuantifiedComparison(ctx.left, ctx.comparison_operator(), ctx.right,
                toQuantifier(ctx.quantifier));
    }

    @Override
    public Expression visitComparisonExpression_arithmetic(
            JPQLSelectExpressionParser.ComparisonExpression_arithmeticContext ctx) {
        return handleComparison(ctx.left, ctx.comparison_operator(), ctx.right);
    }

    @Override
    public Expression visitQuantifiedComparisonExpression_arithmetic(
            QuantifiedComparisonExpression_arithmeticContext ctx) {
        return handleQuantifiedComparison(ctx.left, ctx.comparison_operator(), ctx.right,
                toQuantifier(ctx.quantifier));
    }

    @Override
    public Expression visitComparisonExpression_entitytype(
            JPQLSelectExpressionParser.ComparisonExpression_entitytypeContext ctx) {
        return handleComparison(ctx.left, ctx.equality_comparison_operator(), ctx.right);
    }

    @Override
    public Expression visitQuantifiedComparisonExpression_entitytype(
            QuantifiedComparisonExpression_entitytypeContext ctx) {
        return handleQuantifiedComparison(ctx.left, ctx.equality_comparison_operator(), ctx.right,
                toQuantifier(ctx.quantifier));
    }

    @Override
    public Expression visitComparisonExpression_boolean(
            JPQLSelectExpressionParser.ComparisonExpression_booleanContext ctx) {
        return handleComparison(ctx.left, ctx.equality_comparison_operator(), ctx.right);
    }

    @Override
    public Expression visitQuantifiedComparisonExpression_boolean(
            QuantifiedComparisonExpression_booleanContext ctx) {
        return handleQuantifiedComparison(ctx.left, ctx.equality_comparison_operator(), ctx.right,
                toQuantifier(ctx.quantifier));
    }

    @Override
    public Expression visitComparisonExpression_datetime(
            JPQLSelectExpressionParser.ComparisonExpression_datetimeContext ctx) {
        return handleComparison(ctx.left, ctx.comparison_operator(), ctx.right);
    }

    @Override
    public Expression visitQuantifiedComparisonExpression_datetime(
            QuantifiedComparisonExpression_datetimeContext ctx) {
        return handleQuantifiedComparison(ctx.left, ctx.comparison_operator(), ctx.right,
                toQuantifier(ctx.quantifier));
    }

    @Override
    public Expression visitComparisonExpression_entity(
            JPQLSelectExpressionParser.ComparisonExpression_entityContext ctx) {
        return handleComparison(ctx.left, ctx.equality_comparison_operator(), ctx.right);
    }

    @Override
    public Expression visitQuantifiedComparisonExpression_entity(QuantifiedComparisonExpression_entityContext ctx) {
        return handleQuantifiedComparison(ctx.left, ctx.equality_comparison_operator(), ctx.right,
                toQuantifier(ctx.quantifier));
    }

    @Override
    public Expression visitComparisonExpression_enum(
            JPQLSelectExpressionParser.ComparisonExpression_enumContext ctx) {
        return handleComparison(ctx.left, ctx.equality_comparison_operator(), ctx.right);
    }

    BinaryExpressionPredicate handleComparison(ParseTree left, ParseTree comparisonOperator, ParseTree right) {
        BinaryExpressionPredicate pred = (BinaryExpressionPredicate) comparisonOperator.accept(this);
        pred.setLeft(left.accept(this));
        pred.setRight(right.accept(this));
        return pred;
    }

    BinaryExpressionPredicate handleQuantifiedComparison(ParseTree left, ParseTree comparisonOperator,
            ParseTree right, PredicateQuantifier quantifier) {
        QuantifiableBinaryExpressionPredicate pred = (QuantifiableBinaryExpressionPredicate) comparisonOperator
                .accept(this);
        pred.setLeft(left.accept(this));
        pred.setRight(right.accept(this));
        pred.setQuantifier(quantifier);
        return pred;
    }

    @Override
    public Expression visitIdentifier(IdentifierContext ctx) {
        Expression entityLiteral = createEntityTypeLiteral(ctx.Identifier().getText());
        if (entityLiteral != null) {
            return entityLiteral;
        }

        List<PathElementExpression> pathElems = new ArrayList<>();
        pathElems.add(new PropertyExpression(ctx.Identifier().getText()));
        return new PathExpression(pathElems);
    }

    @Override
    public Expression visitEqPredicate(JPQLSelectExpressionParser.EqPredicateContext ctx) {
        return new EqPredicate(false);
    }

    @Override
    public Expression visitNeqPredicate(JPQLSelectExpressionParser.NeqPredicateContext ctx) {
        return new EqPredicate(true);
    }

    @Override
    public Expression visitGtPredicate(JPQLSelectExpressionParser.GtPredicateContext ctx) {
        return new GtPredicate();
    }

    @Override
    public Expression visitGePredicate(JPQLSelectExpressionParser.GePredicateContext ctx) {
        return new GePredicate();
    }

    @Override
    public Expression visitLtPredicate(JPQLSelectExpressionParser.LtPredicateContext ctx) {
        return new LtPredicate();
    }

    @Override
    public Expression visitLePredicate(JPQLSelectExpressionParser.LePredicateContext ctx) {
        return new LePredicate();
    }

    @Override
    public Expression visitExists_expression(JPQLSelectExpressionParser.Exists_expressionContext ctx) {
        return new ExistsPredicate(ctx.identifier().accept(this), ctx.not != null);
    }

    @Override
    protected Expression aggregateResult(Expression aggregate, Expression nextResult) {
        return aggregate == null ? nextResult : aggregate;
    }

    private String unquote(String literal) {
        return literal.substring(1, literal.length() - 1);
    }

    private PathExpression wrapPath(Expression expression) {
        if (expression instanceof PathExpression) {
            return (PathExpression) expression;
        }

        PathExpression p = new PathExpression();
        p.getExpressions().add((PathElementExpression) expression);
        return p;
    }

    private PredicateQuantifier toQuantifier(Token token) {
        PredicateQuantifier quantifier;
        if (token == null) {
            quantifier = PredicateQuantifier.ONE;
        } else {
            switch (token.getType()) {
            case JPQLSelectExpressionLexer.ANY:
            case JPQLSelectExpressionLexer.SOME:
                quantifier = PredicateQuantifier.ANY;
                break;
            case JPQLSelectExpressionLexer.ALL:
                quantifier = PredicateQuantifier.ALL;
                break;
            default:
                quantifier = PredicateQuantifier.ONE;
            }
        }
        return quantifier;
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    private Expression createEnumLiteral(String enumStr) {
        int lastDotIdx = enumStr.lastIndexOf('.');
        String enumTypeStr = enumStr.substring(0, lastDotIdx);
        String enumValueStr = enumStr.substring(lastDotIdx + 1);
        Class<Enum<?>> enumType = enums.get(enumTypeStr);
        if (enumType == null) {
            return null;
        }
        return new EnumLiteral(Enum.valueOf((Class) enumType, enumValueStr), enumStr);
    }

    private Expression createEntityTypeLiteral(String entityLiteralStr) {
        Class<?> entityType = entities.get(entityLiteralStr);
        if (entityType == null) {
            return null;
        }
        return new EntityLiteral(entityType, entityLiteralStr);
    }
}