/*
 * Copyright (c) 2006, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation
//
package org.eclipse.persistence.jpa.jpql;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.persistence.jpa.jpql.parser.AbsExpression;
import org.eclipse.persistence.jpa.jpql.parser.AbstractConditionalClause;
import org.eclipse.persistence.jpa.jpql.parser.AbstractDoubleEncapsulatedExpression;
import org.eclipse.persistence.jpa.jpql.parser.AbstractEncapsulatedExpression;
import org.eclipse.persistence.jpa.jpql.parser.AbstractExpressionVisitor;
import org.eclipse.persistence.jpa.jpql.parser.AbstractFromClause;
import org.eclipse.persistence.jpa.jpql.parser.AbstractPathExpression;
import org.eclipse.persistence.jpa.jpql.parser.AbstractSchemaName;
import org.eclipse.persistence.jpa.jpql.parser.AbstractSelectClause;
import org.eclipse.persistence.jpa.jpql.parser.AbstractSelectStatement;
import org.eclipse.persistence.jpa.jpql.parser.AbstractSingleEncapsulatedExpression;
import org.eclipse.persistence.jpa.jpql.parser.AbstractTripleEncapsulatedExpression;
import org.eclipse.persistence.jpa.jpql.parser.AdditionExpression;
import org.eclipse.persistence.jpa.jpql.parser.AggregateFunction;
import org.eclipse.persistence.jpa.jpql.parser.AllOrAnyExpression;
import org.eclipse.persistence.jpa.jpql.parser.AndExpression;
import org.eclipse.persistence.jpa.jpql.parser.ArithmeticExpression;
import org.eclipse.persistence.jpa.jpql.parser.ArithmeticExpressionBNF;
import org.eclipse.persistence.jpa.jpql.parser.ArithmeticFactor;
import org.eclipse.persistence.jpa.jpql.parser.ArithmeticPrimaryBNF;
import org.eclipse.persistence.jpa.jpql.parser.ArithmeticTermBNF;
import org.eclipse.persistence.jpa.jpql.parser.AvgFunction;
import org.eclipse.persistence.jpa.jpql.parser.BadExpression;
import org.eclipse.persistence.jpa.jpql.parser.BetweenExpression;
import org.eclipse.persistence.jpa.jpql.parser.CaseExpression;
import org.eclipse.persistence.jpa.jpql.parser.CoalesceExpression;
import org.eclipse.persistence.jpa.jpql.parser.CollectionExpression;
import org.eclipse.persistence.jpa.jpql.parser.CollectionMemberDeclaration;
import org.eclipse.persistence.jpa.jpql.parser.CollectionMemberExpression;
import org.eclipse.persistence.jpa.jpql.parser.CollectionValuedPathExpression;
import org.eclipse.persistence.jpa.jpql.parser.CollectionValuedPathExpressionBNF;
import org.eclipse.persistence.jpa.jpql.parser.ComparisonExpression;
import org.eclipse.persistence.jpa.jpql.parser.CompoundExpression;
import org.eclipse.persistence.jpa.jpql.parser.ConcatExpression;
import org.eclipse.persistence.jpa.jpql.parser.ConditionalExpressionBNF;
import org.eclipse.persistence.jpa.jpql.parser.ConstructorExpression;
import org.eclipse.persistence.jpa.jpql.parser.CountFunction;
import org.eclipse.persistence.jpa.jpql.parser.DateTime;
import org.eclipse.persistence.jpa.jpql.parser.DeleteClause;
import org.eclipse.persistence.jpa.jpql.parser.DeleteStatement;
import org.eclipse.persistence.jpa.jpql.parser.DivisionExpression;
import org.eclipse.persistence.jpa.jpql.parser.EmptyCollectionComparisonExpression;
import org.eclipse.persistence.jpa.jpql.parser.EntityTypeLiteral;
import org.eclipse.persistence.jpa.jpql.parser.EntryExpression;
import org.eclipse.persistence.jpa.jpql.parser.ExistsExpression;
import org.eclipse.persistence.jpa.jpql.parser.Expression;
import org.eclipse.persistence.jpa.jpql.parser.FromClause;
import org.eclipse.persistence.jpa.jpql.parser.FunctionExpression;
import org.eclipse.persistence.jpa.jpql.parser.GroupByClause;
import org.eclipse.persistence.jpa.jpql.parser.HavingClause;
import org.eclipse.persistence.jpa.jpql.parser.IdentificationVariable;
import org.eclipse.persistence.jpa.jpql.parser.IdentificationVariableBNF;
import org.eclipse.persistence.jpa.jpql.parser.IdentificationVariableDeclaration;
import org.eclipse.persistence.jpa.jpql.parser.InExpression;
import org.eclipse.persistence.jpa.jpql.parser.IndexExpression;
import org.eclipse.persistence.jpa.jpql.parser.InputParameter;
import org.eclipse.persistence.jpa.jpql.parser.InternalOrderByItemBNF;
import org.eclipse.persistence.jpa.jpql.parser.JPQLExpression;
import org.eclipse.persistence.jpa.jpql.parser.JPQLGrammar;
import org.eclipse.persistence.jpa.jpql.parser.JPQLQueryBNF;
import org.eclipse.persistence.jpa.jpql.parser.Join;
import org.eclipse.persistence.jpa.jpql.parser.JoinAssociationPathExpressionBNF;
import org.eclipse.persistence.jpa.jpql.parser.KeyExpression;
import org.eclipse.persistence.jpa.jpql.parser.KeywordExpression;
import org.eclipse.persistence.jpa.jpql.parser.LengthExpression;
import org.eclipse.persistence.jpa.jpql.parser.LikeExpression;
import org.eclipse.persistence.jpa.jpql.parser.LikeExpressionEscapeCharacterBNF;
import org.eclipse.persistence.jpa.jpql.parser.LocateExpression;
import org.eclipse.persistence.jpa.jpql.parser.LogicalExpression;
import org.eclipse.persistence.jpa.jpql.parser.LowerExpression;
import org.eclipse.persistence.jpa.jpql.parser.MaxFunction;
import org.eclipse.persistence.jpa.jpql.parser.MinFunction;
import org.eclipse.persistence.jpa.jpql.parser.ModExpression;
import org.eclipse.persistence.jpa.jpql.parser.MultiplicationExpression;
import org.eclipse.persistence.jpa.jpql.parser.NotExpression;
import org.eclipse.persistence.jpa.jpql.parser.NullComparisonExpression;
import org.eclipse.persistence.jpa.jpql.parser.NullExpression;
import org.eclipse.persistence.jpa.jpql.parser.NullIfExpression;
import org.eclipse.persistence.jpa.jpql.parser.NumericLiteral;
import org.eclipse.persistence.jpa.jpql.parser.ObjectExpression;
import org.eclipse.persistence.jpa.jpql.parser.OnClause;
import org.eclipse.persistence.jpa.jpql.parser.OrExpression;
import org.eclipse.persistence.jpa.jpql.parser.OrderByClause;
import org.eclipse.persistence.jpa.jpql.parser.OrderByItem;
import org.eclipse.persistence.jpa.jpql.parser.RangeDeclarationBNF;
import org.eclipse.persistence.jpa.jpql.parser.RangeVariableDeclaration;
import org.eclipse.persistence.jpa.jpql.parser.ResultVariable;
import org.eclipse.persistence.jpa.jpql.parser.SelectClause;
import org.eclipse.persistence.jpa.jpql.parser.SelectStatement;
import org.eclipse.persistence.jpa.jpql.parser.SimpleFromClause;
import org.eclipse.persistence.jpa.jpql.parser.SimpleSelectClause;
import org.eclipse.persistence.jpa.jpql.parser.SimpleSelectStatement;
import org.eclipse.persistence.jpa.jpql.parser.SizeExpression;
import org.eclipse.persistence.jpa.jpql.parser.SqrtExpression;
import org.eclipse.persistence.jpa.jpql.parser.StateFieldPathExpression;
import org.eclipse.persistence.jpa.jpql.parser.StringLiteral;
import org.eclipse.persistence.jpa.jpql.parser.SubExpression;
import org.eclipse.persistence.jpa.jpql.parser.SubstringExpression;
import org.eclipse.persistence.jpa.jpql.parser.SubtractionExpression;
import org.eclipse.persistence.jpa.jpql.parser.SumFunction;
import org.eclipse.persistence.jpa.jpql.parser.TreatExpression;
import org.eclipse.persistence.jpa.jpql.parser.TrimExpression;
import org.eclipse.persistence.jpa.jpql.parser.TypeExpression;
import org.eclipse.persistence.jpa.jpql.parser.UnknownExpression;
import org.eclipse.persistence.jpa.jpql.parser.UpdateClause;
import org.eclipse.persistence.jpa.jpql.parser.UpdateItem;
import org.eclipse.persistence.jpa.jpql.parser.UpdateStatement;
import org.eclipse.persistence.jpa.jpql.parser.UpperExpression;
import org.eclipse.persistence.jpa.jpql.parser.ValueExpression;
import org.eclipse.persistence.jpa.jpql.parser.WhenClause;
import org.eclipse.persistence.jpa.jpql.parser.WhereClause;
import static org.eclipse.persistence.jpa.jpql.JPQLQueryProblemMessages.*;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.*;

/**
 * The base validator responsible to gather the problems found in a JPQL query by validating it
 * against the provided JPQL grammar. The semantic of the JPQL query is not validated by this visitor.
 * <p>
 * Provisional API: This interface is part of an interim API that is still under development and
 * expected to change significantly before reaching stability. It is available at this early stage
 * to solicit feedback from pioneering adopters on the understanding that any code that uses this
 * API will almost certainly be broken (repeatedly) as the API evolves.
 *
 * @see AbstractSemanticValidator
 *
 * @version 2.5.1
 * @since 2.4
 * @author Pascal Filion
 */
@SuppressWarnings("nls")
public abstract class AbstractGrammarValidator extends AbstractValidator {

    /**
     * This visitor determines whether the visited {@link Expression} is the {@link CollectionExpression}.
     */
    private CollectionExpressionVisitor collectionExpressionVisitor;

    /**
     * This validator validates a {@link CollectionExpression} by making sure each item is separated
     * by a comma.
     */
    private CollectionSeparatedByCommaValidator collectionSeparatedByCommaValidator;

    /**
     * This validator validates a {@link CollectionExpression} by making sure each item is separated
     * by a space.
     */
    private CollectionSeparatedBySpaceValidator collectionSeparatedBySpaceValidator;

    /**
     * This visitor is responsible to retrieve the visited {@link Expression} if it is a
     * {@link ComparisonExpression}.
     */
    private ComparisonExpressionVisitor comparisonExpressionVisitor;

    /**
     * The registered expression helper mapped by the unique JPQL identifier of the same expression
     * to validate.
     */
    private Map<String, Object> helpers;

    /**
     * The cached {@link InputParameter InputParameters} that are present in the entire JPQL query.
     */
    private Collection<InputParameter> inputParameters;

    /**
     * The {@link JPQLGrammar} that defines how the JPQL query was parsed.
     */
    private JPQLGrammar jpqlGrammar;

    /**
     * Creates a new <code>AbstractGrammarValidator</code>.
     *
     * @param jpqlGrammar The {@link JPQLGrammar} that defines how the JPQL query was parsed, which
     * cannot be <code>null</code>
     * @exception NullPointerException If the given {@link JPQLGrammar} is <code>null</code>
     */
    protected AbstractGrammarValidator(JPQLGrammar jpqlGrammar) {
        super();
        Assert.isNotNull(jpqlGrammar, "The JPQLGrammar cannot be null");
        this.jpqlGrammar = jpqlGrammar;
    }

    protected AbstractSingleEncapsulatedExpressionHelper<AbsExpression> absExpressionHelper() {
        AbstractSingleEncapsulatedExpressionHelper<AbsExpression> helper = getHelper(ABS);
        if (helper == null) {
            helper = buildAbsExpressionHelper();
            registerHelper(ABS, helper);
        }
        return helper;
    }

    protected AbstractSingleEncapsulatedExpressionHelper<AllOrAnyExpression> allOrAnyExpressionHelper() {
        AbstractSingleEncapsulatedExpressionHelper<AllOrAnyExpression> helper = getHelper(ALL);
        if (helper == null) {
            helper = buildAllOrAnyExpressionHelper();
            registerHelper(ALL, helper);
        }
        return helper;
    }

    protected AbstractSingleEncapsulatedExpressionHelper<AvgFunction> avgFunctionHelper() {
        AbstractSingleEncapsulatedExpressionHelper<AvgFunction> helper = getHelper(AVG);
        if (helper == null) {
            helper = buildAvgFunctionHelper();
            registerHelper(AVG, helper);
        }
        return helper;
    }

    protected AbstractSingleEncapsulatedExpressionHelper<AbsExpression> buildAbsExpressionHelper() {
        return new AbstractSingleEncapsulatedExpressionHelper<AbsExpression>(this) {
            @Override
            public String encapsulatedExpressionInvalidKey(AbsExpression expression) {
                return AbsExpression_InvalidExpression;
            }
            @Override
            public String encapsulatedExpressionMissingKey(AbsExpression expression) {
                return AbsExpression_MissingExpression;
            }
            public String leftParenthesisMissingKey(AbsExpression expression) {
                return AbsExpression_MissingLeftParenthesis;
            }
            public String rightParenthesisMissingKey(AbsExpression expression) {
                return AbsExpression_MissingRightParenthesis;
            }
        };
    }

    protected AbstractSingleEncapsulatedExpressionHelper<AllOrAnyExpression> buildAllOrAnyExpressionHelper() {
        return new AbstractSingleEncapsulatedExpressionHelper<AllOrAnyExpression>(this) {
            @Override
            public String[] arguments(AllOrAnyExpression expression) {
                return new String[] { expression.getIdentifier() };
            }
            @Override
            public String encapsulatedExpressionInvalidKey(AllOrAnyExpression expression) {
                return AllOrAnyExpression_InvalidExpression;
            }
            @Override
            public String encapsulatedExpressionMissingKey(AllOrAnyExpression expression) {
                return AllOrAnyExpression_MissingExpression;
            }
            public String leftParenthesisMissingKey(AllOrAnyExpression expression) {
                return AllOrAnyExpression_MissingLeftParenthesis;
            }
            public String rightParenthesisMissingKey(AllOrAnyExpression expression) {
                return AllOrAnyExpression_MissingRightParenthesis;
            }
        };
    }

    protected AbstractSingleEncapsulatedExpressionHelper<AvgFunction> buildAvgFunctionHelper() {
        return new AbstractSingleEncapsulatedExpressionHelper<AvgFunction>(this) {
            @Override
            public String encapsulatedExpressionInvalidKey(AvgFunction expression) {
                return AvgFunction_InvalidExpression;
            }
            @Override
            public String encapsulatedExpressionMissingKey(AvgFunction expression) {
                return AvgFunction_MissingExpression;
            }
            public String leftParenthesisMissingKey(AvgFunction expression) {
                return AvgFunction_MissingLeftParenthesis;
            }
            @Override
            public int lengthBeforeEncapsulatedExpression(AvgFunction expression) {
                return (expression.hasDistinct() ? 8 /* DISTINCT */ : 0) +
                       (expression.hasSpaceAfterDistinct() ? 1 : 0);
            }
            public String rightParenthesisMissingKey(AvgFunction expression) {
                return AvgFunction_MissingRightParenthesis;
            }
        };
    }

    protected AbstractSingleEncapsulatedExpressionHelper<CoalesceExpression> buildCoalesceExpressionHelper() {
        return new AbstractSingleEncapsulatedExpressionHelper<CoalesceExpression>(this) {
            @Override
            public String encapsulatedExpressionInvalidKey(CoalesceExpression expression) {
                return CoalesceExpression_InvalidExpression;
            }
            @Override
            public String encapsulatedExpressionMissingKey(CoalesceExpression expression) {
                return CoalesceExpression_MissingExpression;
            }
            @Override
            protected boolean isEncapsulatedExpressionValid(CoalesceExpression expression) {
                return isValidWithChildCollectionBypass(expression.getExpression(), expression.getEncapsulatedExpressionQueryBNFId());
            }
            public String leftParenthesisMissingKey(CoalesceExpression expression) {
                return CoalesceExpression_MissingLeftParenthesis;
            }
            public String rightParenthesisMissingKey(CoalesceExpression expression) {
                return CoalesceExpression_MissingRightParenthesis;
            }
        };
    }

    /**
     * Creates a visitor that collects the {@link CollectionExpression} if it's been visited.
     *
     * @return A new {@link CollectionExpressionVisitor}
     */
    protected CollectionExpressionVisitor buildCollectionExpressionVisitor() {
        return new CollectionExpressionVisitor();
    }

    protected AbstractSingleEncapsulatedExpressionHelper<ConcatExpression> buildConcatExpressionHelper() {
        return new AbstractSingleEncapsulatedExpressionHelper<ConcatExpression>(this) {
            @Override
            public String encapsulatedExpressionInvalidKey(ConcatExpression expression) {
                return ConcatExpression_InvalidExpression;
            }
            @Override
            public String encapsulatedExpressionMissingKey(ConcatExpression expression) {
                return ConcatExpression_MissingExpression;
            }
            @Override
            public boolean isEncapsulatedExpressionValid(ConcatExpression expression) {
                // Done in visit(ConcatExpression)
                return true;
            }
            public String leftParenthesisMissingKey(ConcatExpression expression) {
                return ConcatExpression_MissingLeftParenthesis;
            }
            public String rightParenthesisMissingKey(ConcatExpression expression) {
                return ConcatExpression_MissingRightParenthesis;
            }
        };
    }

    protected AbstractSingleEncapsulatedExpressionHelper<CountFunction> buildCountFunctionHelper() {
        return new AbstractSingleEncapsulatedExpressionHelper<CountFunction>(this) {
            @Override
            public String encapsulatedExpressionInvalidKey(CountFunction expression) {
                return CountFunction_InvalidExpression;
            }
            @Override
            public String encapsulatedExpressionMissingKey(CountFunction expression) {
                return CountFunction_MissingExpression;
            }
            public String leftParenthesisMissingKey(CountFunction expression) {
                return CountFunction_MissingLeftParenthesis;
            }
            @Override
            public int lengthBeforeEncapsulatedExpression(CountFunction expression) {
                return (expression.hasDistinct() ? 8 /* DISTINCT */ : 0) +
                       (expression.hasSpaceAfterDistinct() ? 1 : 0);
            }
            public String rightParenthesisMissingKey(CountFunction expression) {
                return CountFunction_MissingRightParenthesis;
            }
        };
    }

    protected DateTimeVisitor buildDateTimeVisitor() {
        return new DateTimeVisitor();
    }

    protected AbstractSingleEncapsulatedExpressionHelper<EntryExpression> buildEntryExpressionHelper() {
        return new AbstractSingleEncapsulatedExpressionHelper<EntryExpression>(this) {
            @Override
            public String encapsulatedExpressionInvalidKey(EntryExpression expression) {
                return EntryExpression_InvalidExpression;
            }
            @Override
            public String encapsulatedExpressionMissingKey(EntryExpression expression) {
                return EntryExpression_MissingExpression;
            }
            @Override
            public boolean isEncapsulatedExpressionValid(EntryExpression expression) {
                return isValid(expression.getExpression(), IdentificationVariableBNF.ID);
            }
            public String leftParenthesisMissingKey(EntryExpression expression) {
                return EntryExpression_MissingLeftParenthesis;
            }
            public String rightParenthesisMissingKey(EntryExpression expression) {
                return EntryExpression_MissingRightParenthesis;
            }
        };
    }

    protected AbstractSingleEncapsulatedExpressionHelper<ExistsExpression> buildExistsExpressionHelper() {
        return new AbstractSingleEncapsulatedExpressionHelper<ExistsExpression>(this) {
            @Override
            public String encapsulatedExpressionInvalidKey(ExistsExpression expression) {
                return ExistsExpression_InvalidExpression;
            }
            @Override
            public String encapsulatedExpressionMissingKey(ExistsExpression expression) {
                return ExistsExpression_MissingExpression;
            }
            public String leftParenthesisMissingKey(ExistsExpression expression) {
                return ExistsExpression_MissingLeftParenthesis;
            }
            public String rightParenthesisMissingKey(ExistsExpression expression) {
                return ExistsExpression_MissingRightParenthesis;
            }
        };
    }

    protected AbstractSingleEncapsulatedExpressionHelper<FunctionExpression> buildFunctionExpressionHelper() {
        return new AbstractSingleEncapsulatedExpressionHelper<FunctionExpression>(this) {
            @Override
            public String[] arguments(FunctionExpression expression) {
                return new String[] { expression.getIdentifier() };
            }
            @Override
            @SuppressWarnings("fallthrough")
            protected String encapsulatedExpressionInvalidKey(FunctionExpression expression) {
                switch (expression.getParameterCount()) {
                    case ONE: {
                        Expression children = expression.getExpression();
                        int childrenCount = getChildren(children).size();
                        if (childrenCount > 1) {
                            return FunctionExpression_MoreThanOneExpression;
                        }
                    }
                    case ZERO: {
                        return FunctionExpression_HasExpression;
                    }
                    default: {
                        return FunctionExpression_InvalidExpression;
                    }
                }
            }
            @Override
            @SuppressWarnings("fallthrough")
            protected String encapsulatedExpressionMissingKey(FunctionExpression expression) {
                switch (expression.getParameterCount()) {
                    case ONE: {
                        Expression children = expression.getExpression();
                        int childrenCount = getChildren(children).size();
                        if (childrenCount == 0) {
                            return FunctionExpression_MissingOneExpression;
                        }
                    }
                    default: {
                        return FunctionExpression_MissingExpression;
                    }
                }
            }
            @Override
            protected boolean isEncapsulatedExpressionMissing(FunctionExpression expression) {
                switch (expression.getParameterCount()) {
                    case ONE:
                    case ONE_OR_MANY: return !expression.hasExpression();
                    default:          return false;
                }
            }
            @Override
            protected boolean isEncapsulatedExpressionValid(FunctionExpression expression) {
                switch (expression.getParameterCount()) {
                    case ONE: {
                        return isValid(
                            expression.getExpression(),
                            expression.getEncapsulatedExpressionQueryBNFId()
                        );
                    }
                    case ONE_OR_MANY: {
                        return isValidWithChildCollectionBypass(
                            expression.getExpression(),
                            expression.getEncapsulatedExpressionQueryBNFId()
                        );
                    }
                    case ZERO_OR_ONE: {
                        return !expression.hasExpression() ||
                               isValid(expression.getExpression(), expression.getEncapsulatedExpressionQueryBNFId());
                    }
                    default: {
                        return true;
                    }
                }
            }
            public String leftParenthesisMissingKey(FunctionExpression expression) {
                return null; // never happens
            }
            @Override
            protected int lengthBeforeEncapsulatedExpression(FunctionExpression expression) {
                return expression.getFunctionName().length() +
                       (expression.hasComma() ? 1 : 0) +
                       (expression.hasSpaceAfterComma() ? 1 : 0);
            }
            public String rightParenthesisMissingKey(FunctionExpression expression) {
                return FunctionExpression_MissingRightParenthesis;
            }
        };
    }

    protected AbstractSingleEncapsulatedExpressionHelper<IndexExpression> buildIndexExpressionHelper() {
        return new AbstractSingleEncapsulatedExpressionHelper<IndexExpression>(this) {
            @Override
            public String encapsulatedExpressionInvalidKey(IndexExpression expression) {
                return IndexExpression_InvalidExpression;
            }
            @Override
            public String encapsulatedExpressionMissingKey(IndexExpression expression) {
                return IndexExpression_MissingExpression;
            }
            @Override
            public boolean isEncapsulatedExpressionValid(IndexExpression expression) {
                return isValid(expression.getExpression(), IdentificationVariableBNF.ID);
            }
            public String leftParenthesisMissingKey(IndexExpression expression) {
                return IndexExpression_MissingLeftParenthesis;
            }
            public String rightParenthesisMissingKey(IndexExpression expression) {
                return IndexExpression_MissingRightParenthesis;
            }
        };
    }

    protected AbstractSingleEncapsulatedExpressionHelper<KeyExpression> buildKeyExpressionHelper() {
        return new AbstractSingleEncapsulatedExpressionHelper<KeyExpression>(this) {
            @Override
            public String encapsulatedExpressionInvalidKey(KeyExpression expression) {
                return KeyExpression_InvalidExpression;
            }
            @Override
            public String encapsulatedExpressionMissingKey(KeyExpression expression) {
                return KeyExpression_MissingExpression;
            }
            @Override
            public boolean isEncapsulatedExpressionValid(KeyExpression expression) {
                return isValid(expression.getExpression(), IdentificationVariableBNF.ID);
            }
            public String leftParenthesisMissingKey(KeyExpression expression) {
                return KeyExpression_MissingLeftParenthesis;
            }
            public String rightParenthesisMissingKey(KeyExpression expression) {
                return KeyExpression_MissingRightParenthesis;
            }
        };
    }

    protected AbstractSingleEncapsulatedExpressionHelper<LengthExpression> buildLengthExpressionHelper() {
        return new AbstractSingleEncapsulatedExpressionHelper<LengthExpression>(this) {
            @Override
            public String encapsulatedExpressionInvalidKey(LengthExpression expression) {
                return LengthExpression_InvalidExpression;
            }
            @Override
            public String encapsulatedExpressionMissingKey(LengthExpression expression) {
                return LengthExpression_MissingExpression;
            }
            public String leftParenthesisMissingKey(LengthExpression expression) {
                return LengthExpression_MissingLeftParenthesis;
            }
            public String rightParenthesisMissingKey(LengthExpression expression) {
                return LengthExpression_MissingRightParenthesis;
            }
        };
    }

    protected AbstractTripleEncapsulatedExpressionHelper<LocateExpression> buildLocateExpressionHelper() {
        return new AbstractTripleEncapsulatedExpressionHelper<LocateExpression>(this) {
            @Override
            protected String firstCommaMissingKey() {
                return LocateExpression_MissingFirstComma;
            }
            @Override
            protected String firstExpressionInvalidKey() {
                return LocateExpression_InvalidFirstExpression;
            }
            @Override
            protected String firstExpressionMissingKey() {
                return LocateExpression_MissingFirstExpression;
            }
            public String identifier(LocateExpression expression) {
                return LOCATE;
            }
            public String leftParenthesisMissingKey(LocateExpression expression) {
                return LocateExpression_MissingLeftParenthesis;
            }
            public String rightParenthesisMissingKey(LocateExpression expression) {
                return LocateExpression_MissingRightParenthesis;
            }
            @Override
            protected String secondCommaMissingKey() {
                return LocateExpression_MissingSecondComma;
            }
            @Override
            protected String secondExpressionInvalidKey() {
                return LocateExpression_InvalidSecondExpression;
            }
            @Override
            protected String secondExpressionMissingKey() {
                return LocateExpression_MissingSecondExpression;
            }
            @Override
            protected String thirdExpressionInvalidKey() {
                return LocateExpression_InvalidThirdExpression;
            }
            @Override
            protected String thirdExpressionMissingKey() {
                return LocateExpression_MissingThirdExpression;
            }
        };
    }

    protected AbstractSingleEncapsulatedExpressionHelper<LowerExpression> buildLowerExpressionHelper() {
        return new AbstractSingleEncapsulatedExpressionHelper<LowerExpression>(this) {
            @Override
            public String encapsulatedExpressionInvalidKey(LowerExpression expression) {
                return LowerExpression_InvalidExpression;
            }
            @Override
            public String encapsulatedExpressionMissingKey(LowerExpression expression) {
                return LowerExpression_MissingExpression;
            }
            public String leftParenthesisMissingKey(LowerExpression expression) {
                return LowerExpression_MissingLeftParenthesis;
            }
            public String rightParenthesisMissingKey(LowerExpression expression) {
                return LowerExpression_MissingRightParenthesis;
            }
        };
    }

    protected AbstractSingleEncapsulatedExpressionHelper<MaxFunction> buildMaxFunctionHelper() {
        return new AbstractSingleEncapsulatedExpressionHelper<MaxFunction>(this) {
            @Override
            public String encapsulatedExpressionInvalidKey(MaxFunction expression) {
                return MaxFunction_InvalidExpression;
            }
            @Override
            public String encapsulatedExpressionMissingKey(MaxFunction expression) {
                return MaxFunction_MissingExpression;
            }
            public String leftParenthesisMissingKey(MaxFunction expression) {
                return MaxFunction_MissingLeftParenthesis;
            }
            @Override
            public int lengthBeforeEncapsulatedExpression(MaxFunction expression) {
                return (expression.hasDistinct() ? 8 /* DISTINCT */ : 0) +
                       (expression.hasSpaceAfterDistinct() ? 1 : 0);
            }
            public String rightParenthesisMissingKey(MaxFunction expression) {
                return MaxFunction_MissingRightParenthesis;
            }
        };
    }

    protected AbstractSingleEncapsulatedExpressionHelper<MinFunction> buildMinFunctionHelper() {
        return new AbstractSingleEncapsulatedExpressionHelper<MinFunction>(this) {
            @Override
            public String encapsulatedExpressionInvalidKey(MinFunction expression) {
                return MinFunction_InvalidExpression;
            }
            @Override
            public String encapsulatedExpressionMissingKey(MinFunction expression) {
                return MinFunction_MissingExpression;
            }
            public String leftParenthesisMissingKey(MinFunction expression) {
                return MinFunction_MissingLeftParenthesis;
            }
            @Override
            public int lengthBeforeEncapsulatedExpression(MinFunction expression) {
                return (expression.hasDistinct() ? 8 /* DISTINCT */ : 0) +
                       (expression.hasSpaceAfterDistinct() ? 1 : 0);
            }
            public String rightParenthesisMissingKey(MinFunction expression) {
                return MinFunction_MissingRightParenthesis;
            }
        };
    }

    protected AbstractDoubleEncapsulatedExpressionHelper<ModExpression> buildModExpressionHelper() {
        return new AbstractDoubleEncapsulatedExpressionHelper<ModExpression>(this) {
            @Override
            protected String firstExpressionInvalidKey() {
                return ModExpression_InvalidFirstExpression;
            }
            @Override
            protected String firstExpressionMissingKey() {
                return ModExpression_MissingFirstExpression;
            }
            public String leftParenthesisMissingKey(ModExpression expression) {
                return ModExpression_MissingLeftParenthesis;
            }
            @Override
            protected String missingCommaKey() {
                return ModExpression_MissingComma;
            }
            public String rightParenthesisMissingKey(ModExpression expression) {
                return ModExpression_MissingRightParenthesis;
            }
            @Override
            protected String secondExpressionInvalidKey() {
                return ModExpression_InvalidSecondExpression;
            }
            @Override
            protected String secondExpressionMissingKey() {
                return ModExpression_MissingSecondExpression;
            }
        };
    }

    /**
     * Creates a visitor that collects the {@link NullExpression} if it's been visited.
     *
     * @return A new {@link NullExpressionVisitor}
     */
    protected NullExpressionVisitor buildNullExpressionVisitor() {
        return new NullExpressionVisitor();
    }

    protected AbstractDoubleEncapsulatedExpressionHelper<NullIfExpression> buildNullIfExpressionHelper() {
        return new AbstractDoubleEncapsulatedExpressionHelper<NullIfExpression>(this) {
            @Override
            public String firstExpressionInvalidKey() {
                return NullIfExpression_InvalidFirstExpression;
            }
            @Override
            public String firstExpressionMissingKey() {
                return NullIfExpression_MissingFirstExpression;
            }
            public String leftParenthesisMissingKey(NullIfExpression expression) {
                return NullIfExpression_MissingLeftParenthesis;
            }
            @Override
            public String missingCommaKey() {
                return NullIfExpression_MissingComma;
            }
            public String rightParenthesisMissingKey(NullIfExpression expression) {
                return NullIfExpression_MissingRightParenthesis;
            }
            @Override
            public String secondExpressionInvalidKey() {
                return NullIfExpression_InvalidSecondExpression;
            }
            @Override
            public String secondExpressionMissingKey() {
                return NullIfExpression_MissingSecondExpression;
            }
        };
    }

    protected AbstractSingleEncapsulatedExpressionHelper<ObjectExpression> buildObjectExpressionHelper() {
        return new AbstractSingleEncapsulatedExpressionHelper<ObjectExpression>(this) {
            @Override
            public String encapsulatedExpressionInvalidKey(ObjectExpression expression) {
                return ObjectExpression_InvalidExpression;
            }
            @Override
            public String encapsulatedExpressionMissingKey(ObjectExpression expression) {
                return ObjectExpression_MissingExpression;
            }
            @Override
            public boolean isEncapsulatedExpressionValid(ObjectExpression expression) {
                return isValid(expression.getExpression(), IdentificationVariableBNF.ID);
            }
            public String leftParenthesisMissingKey(ObjectExpression expression) {
                return ObjectExpression_MissingLeftParenthesis;
            }
            public String rightParenthesisMissingKey(ObjectExpression expression) {
                return ObjectExpression_MissingRightParenthesis;
            }
        };
    }

    protected AbstractSingleEncapsulatedExpressionHelper<SizeExpression> buildSizeExpressionHelper() {
        return new AbstractSingleEncapsulatedExpressionHelper<SizeExpression>(this) {
            @Override
            public String encapsulatedExpressionInvalidKey(SizeExpression expression) {
                return SizeExpression_InvalidExpression;
            }
            @Override
            public String encapsulatedExpressionMissingKey(SizeExpression expression) {
                return SizeExpression_MissingExpression;
            }
            public String leftParenthesisMissingKey(SizeExpression expression) {
                return SizeExpression_MissingLeftParenthesis;
            }
            public String rightParenthesisMissingKey(SizeExpression expression) {
                return SizeExpression_MissingRightParenthesis;
            }
        };
    }

    protected AbstractSingleEncapsulatedExpressionHelper<SqrtExpression> buildSqrtExpressionHelper() {
        return new AbstractSingleEncapsulatedExpressionHelper<SqrtExpression>(this) {
            @Override
            public String encapsulatedExpressionInvalidKey(SqrtExpression expression) {
                return SqrtExpression_InvalidExpression;
            }
            @Override
            public String encapsulatedExpressionMissingKey(SqrtExpression expression) {
                return SqrtExpression_MissingExpression;
            }
            public String leftParenthesisMissingKey(SqrtExpression expression) {
                return SqrtExpression_MissingLeftParenthesis;
            }
            public String rightParenthesisMissingKey(SqrtExpression expression) {
                return SqrtExpression_MissingRightParenthesis;
            }
        };
    }

    protected AbstractTripleEncapsulatedExpressionHelper<SubstringExpression> buildSubstringExpressionHelper() {
        return new AbstractTripleEncapsulatedExpressionHelper<SubstringExpression>(this) {
            @Override
            protected String firstCommaMissingKey() {
                return SubstringExpression_MissingFirstComma;
            }
            @Override
            protected String firstExpressionInvalidKey() {
                return SubstringExpression_InvalidFirstExpression;
            }
            @Override
            protected String firstExpressionMissingKey() {
                return SubstringExpression_MissingFirstExpression;
            }
            public String identifier(SubstringExpression expression) {
                return SUBSTRING;
            }
            public String leftParenthesisMissingKey(SubstringExpression expression) {
                return SubstringExpression_MissingLeftParenthesis;
            }
            public String rightParenthesisMissingKey(SubstringExpression expression) {
                return SubstringExpression_MissingRightParenthesis;
            }
            @Override
            protected String secondCommaMissingKey() {
                return SubstringExpression_MissingSecondComma;
            }
            @Override
            protected String secondExpressionInvalidKey() {
                return SubstringExpression_InvalidSecondExpression;
            }
            @Override
            protected String secondExpressionMissingKey() {
                return SubstringExpression_MissingSecondExpression;
            }
            @Override
            protected String thirdExpressionInvalidKey() {
                return SubstringExpression_InvalidThirdExpression;
            }
            @Override
            protected String thirdExpressionMissingKey() {
                return SubstringExpression_MissingThirdExpression;
            }
        };
    }

    protected AbstractSingleEncapsulatedExpressionHelper<SumFunction> buildSumFunctionHelper() {
        return new AbstractSingleEncapsulatedExpressionHelper<SumFunction>(this) {
            @Override
            public String encapsulatedExpressionInvalidKey(SumFunction expression) {
                return SumFunction_InvalidExpression;
            }
            @Override
            public String encapsulatedExpressionMissingKey(SumFunction expression) {
                return SumFunction_MissingExpression;
            }
            public String leftParenthesisMissingKey(SumFunction expression) {
                return SumFunction_MissingLeftParenthesis;
            }
            @Override
            public int lengthBeforeEncapsulatedExpression(SumFunction expression) {
                return (expression.hasDistinct() ? 8 /* DISTINCT */ : 0) +
                       (expression.hasSpaceAfterDistinct() ? 1 : 0);
            }
            public String rightParenthesisMissingKey(SumFunction expression) {
                return SumFunction_MissingRightParenthesis;
            }
        };
    }

    protected AbstractSingleEncapsulatedExpressionHelper<TrimExpression> buildTrimExpressionHelper() {
        return new AbstractSingleEncapsulatedExpressionHelper<TrimExpression>(this) {
            @Override
            public String encapsulatedExpressionInvalidKey(TrimExpression expression) {
                return TrimExpression_InvalidExpression;
            }
            @Override
            public String encapsulatedExpressionMissingKey(TrimExpression expression) {
                return TrimExpression_MissingExpression;
            }
            @Override
            public boolean isEncapsulatedExpressionMissing(TrimExpression expression) {
                // Done in visit(TrimExpression)
                return false;
            }
            @Override
            public boolean isEncapsulatedExpressionValid(TrimExpression expression) {
                // Done in visit(TrimExpression)
                return true;
            }
            public String leftParenthesisMissingKey(TrimExpression expression) {
                return TrimExpression_MissingLeftParenthesis;
            }
            public String rightParenthesisMissingKey(TrimExpression expression) {
                return TrimExpression_MissingRightParenthesis;
            }
        };
    }

    protected AbstractSingleEncapsulatedExpressionHelper<TypeExpression> buildTypeExpressionHelper() {
        return new AbstractSingleEncapsulatedExpressionHelper<TypeExpression>(this) {
            @Override
            public String encapsulatedExpressionInvalidKey(TypeExpression expression) {
                return TypeExpression_InvalidExpression;
            }
            @Override
            public String encapsulatedExpressionMissingKey(TypeExpression expression) {
                return TypeExpression_MissingExpression;
            }
            public String leftParenthesisMissingKey(TypeExpression expression) {
                return TypeExpression_MissingLeftParenthesis;
            }
            public String rightParenthesisMissingKey(TypeExpression expression) {
                return TypeExpression_MissingRightParenthesis;
            }
        };
    }

    protected AbstractSingleEncapsulatedExpressionHelper<UpperExpression> buildUpperExpressionHelper() {
        return new AbstractSingleEncapsulatedExpressionHelper<UpperExpression>(this) {
            @Override
            public String encapsulatedExpressionInvalidKey(UpperExpression expression) {
                return UpperExpression_InvalidExpression;
            }
            @Override
            public String encapsulatedExpressionMissingKey(UpperExpression expression) {
                return UpperExpression_MissingExpression;
            }
            public String leftParenthesisMissingKey(UpperExpression expression) {
                return UpperExpression_MissingLeftParenthesis;
            }
            public String rightParenthesisMissingKey(UpperExpression expression) {
                return UpperExpression_MissingRightParenthesis;
            }
        };
    }

    protected AbstractSingleEncapsulatedExpressionHelper<ValueExpression> buildValueExpressionHelper() {
        return new AbstractSingleEncapsulatedExpressionHelper<ValueExpression>(this) {
            @Override
            protected String encapsulatedExpressionInvalidKey(ValueExpression expression) {
                return ValueExpression_InvalidExpression;
            }
            @Override
            protected String encapsulatedExpressionMissingKey(ValueExpression expression) {
                return ValueExpression_MissingExpression;
            }
            @Override
            protected boolean isEncapsulatedExpressionValid(ValueExpression expression) {
                return isValid(expression.getExpression(), IdentificationVariableBNF.ID);
            }
            public String leftParenthesisMissingKey(ValueExpression expression) {
                return ValueExpression_MissingLeftParenthesis;
            }
            public String rightParenthesisMissingKey(ValueExpression expression) {
                return ValueExpression_MissingRightParenthesis;
            }
        };
    }

    protected AbstractSingleEncapsulatedExpressionHelper<CoalesceExpression> coalesceExpressionHelper() {
        AbstractSingleEncapsulatedExpressionHelper<CoalesceExpression> helper = getHelper(COALESCE);
        if (helper == null) {
            helper = buildCoalesceExpressionHelper();
            registerHelper(COALESCE, helper);
        }
        return helper;
    }

    protected CollectionSeparatedByCommaValidator collectionSeparatedByCommaValidator() {
        if (collectionSeparatedByCommaValidator == null) {
            collectionSeparatedByCommaValidator = new CollectionSeparatedByCommaValidator(this);
        }
        return collectionSeparatedByCommaValidator;
    }

    protected CollectionSeparatedBySpaceValidator collectionSeparatedBySpaceValidator() {
        if (collectionSeparatedBySpaceValidator == null) {
            collectionSeparatedBySpaceValidator = new CollectionSeparatedBySpaceValidator(this);
        }
        return collectionSeparatedBySpaceValidator;
    }

    protected ComparisonExpressionVisitor comparisonExpressionVisitor() {
        if (comparisonExpressionVisitor == null) {
            comparisonExpressionVisitor = new ComparisonExpressionVisitor();
        }
        return comparisonExpressionVisitor;
    }

    protected AbstractSingleEncapsulatedExpressionHelper<ConcatExpression> concatExpressionHelper() {
        AbstractSingleEncapsulatedExpressionHelper<ConcatExpression> helper = getHelper(CONCAT);
        if (helper == null) {
            helper = buildConcatExpressionHelper();
            registerHelper(CONCAT, helper);
        }
        return helper;
    }

    protected AbstractSingleEncapsulatedExpressionHelper<CountFunction> countFunctionHelper() {
        AbstractSingleEncapsulatedExpressionHelper<CountFunction> helper = getHelper(COUNT);
        if (helper == null) {
            helper = buildCountFunctionHelper();
            registerHelper(COUNT, helper);
        }
        return helper;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void dispose() {
        inputParameters.clear();
        super.dispose();
    }

    protected AbstractSingleEncapsulatedExpressionHelper<EntryExpression> entryExpressionHelper() {
        AbstractSingleEncapsulatedExpressionHelper<EntryExpression> helper = getHelper(ENTRY);
        if (helper == null) {
            helper = buildEntryExpressionHelper();
            registerHelper(ENTRY, helper);
        }
        return helper;
    }

    protected AbstractSingleEncapsulatedExpressionHelper<ExistsExpression> existsExpressionHelper() {
        AbstractSingleEncapsulatedExpressionHelper<ExistsExpression> helper = getHelper(EXISTS);
        if (helper == null) {
            helper = buildExistsExpressionHelper();
            registerHelper(EXISTS, helper);
        }
        return helper;
    }

    protected AbstractSingleEncapsulatedExpressionHelper<FunctionExpression> functionExpressionHelper() {
        AbstractSingleEncapsulatedExpressionHelper<FunctionExpression> helper = getHelper(FUNCTION);
        if (helper == null) {
            helper = buildFunctionExpressionHelper();
            registerHelper(FUNCTION, helper);
        }
        return helper;
    }

    /**
     * Casts the given {@link Expression} to a {@link CollectionExpression} if it is actually an
     * object of that type.
     *
     * @param expression The {@link Expression} to cast
     * @return The given {@link Expression} if it is a {@link CollectionExpression} or <code>null</code>
     * if it is any other object
     */
    protected CollectionExpression getCollectionExpression(Expression expression) {
        CollectionExpressionVisitor visitor = getCollectionExpressionVisitor();
        try {
            expression.accept(visitor);
            return visitor.expression;
        }
        finally {
            visitor.expression = null;
        }
    }

    /**
     * Returns the visitor that collects the {@link CollectionExpression} if it's been visited.
     *
     * @return The {@link CollectionExpressionVisitor}
     * @see #buildCollectionExpressionVisitor()
     */
    protected CollectionExpressionVisitor getCollectionExpressionVisitor() {
        if (collectionExpressionVisitor == null) {
            collectionExpressionVisitor = buildCollectionExpressionVisitor();
        }
        return collectionExpressionVisitor;
    }

    protected DateTimeVisitor getDateTimeVisitor() {
        DateTimeVisitor visitor = getHelper(DateTime.CURRENT_TIMESTAMP);
        if (visitor == null) {
            visitor = buildDateTimeVisitor();
            registerHelper(DateTime.CURRENT_TIMESTAMP, visitor);
        }
        return visitor;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected JPQLGrammar getGrammar() {
        return jpqlGrammar;
    }

    /**
     * Returns the registered helper that was cached with the given id.
     *
     * @param id The key used to retrieve the cached helper, if one was cached
     * @return Either the cached helper or <code>null</code> if no helper was previously cached for
     * the given id
     */
    @SuppressWarnings("unchecked")
    protected <T> T getHelper(String id) {
        return (T) helpers.get(id);
    }

    protected AbstractSingleEncapsulatedExpressionHelper<IndexExpression> indexExpressionHelper() {
        AbstractSingleEncapsulatedExpressionHelper<IndexExpression> helper = getHelper(INDEX);
        if (helper == null) {
            helper = buildIndexExpressionHelper();
            registerHelper(INDEX, helper);
        }
        return helper;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void initialize() {
        super.initialize();
        helpers         = new HashMap<String, Object>();
        inputParameters = new ArrayList<InputParameter>();
    }

    protected boolean isChildOfComparisonExpession(AllOrAnyExpression expression) {
        ComparisonExpressionVisitor visitor = comparisonExpressionVisitor();
        BypassParentSubExpressionVisitor bypassVisitor = getBypassParentSubExpressionVisitor();
        try {
            bypassVisitor.visitor = visitor;
            expression.getParent().accept(visitor);
            return visitor.expression != null;
        }
        finally {
            bypassVisitor.visitor = null;
            visitor.expression = null;
        }
    }

    /**
     * Determines whether the given {@link Expression} is a {@link CollectionExpression}.
     *
     * @param expression The {@link Expression} to verify
     * @return <code>true</code> if the given given {@link Expression} is a {@link CollectionExpression};
     * <code>false</code> otherwise
     */
    protected boolean isCollectionExpression(Expression expression) {
        return getCollectionExpression(expression) != null;
    }

    /**
     * Determines whether the given {@link Expression} represents one of the three date constants or not.
     *
     * @param leftExpression The {@link Expression} to visit
     * @return <code>true</code> if the given {@link Expression} represents one of the tree date
     * constants; <code>false</code> otherwise
     */
    protected boolean isDateTimeConstant(Expression leftExpression) {
        DateTimeVisitor visitor = getDateTimeVisitor();
        try {
            leftExpression.accept(visitor);
            return visitor.dateTime;
        }
        finally {
            visitor.dateTime = false;
        }
    }

    protected boolean isInputParameterInValidLocation(InputParameter expression) {
        OwningClauseVisitor visitor = getOwningClauseVisitor();
        try {
            expression.accept(visitor);
            return visitor.whereClause  != null ||
                   visitor.havingClause != null;
        }
        finally {
            visitor.dispose();
        }
    }

    /**
     * Determines whether a <code><b>JOIN FETCH</b></code> expression can be identified by with an
     * identification variable or not.
     *
     * @return <code>true</code> if the expression can have an identification variable;
     * <code>false</code> otherwise
     */
    protected abstract boolean isJoinFetchIdentifiable();

    /**
     * Determines whether the JPA version defined by the JPQL grammar is 1.0.
     *
     * @return <code>true</code> if the JPQL grammar was defined for JPA 1.0; <code>false</code> if
     * it was defined for a more recent version
     */
    protected boolean isJPA1_0() {
        return getJPAVersion() == JPAVersion.VERSION_1_0;
    }

    /**
     * Determines whether the JPA version defined by the JPQL grammar is 2.0.
     *
     * @return <code>true</code> if the JPQL grammar was defined for JPA 2.0; <code>false</code> if
     * it was defined for a more recent version
     */
    protected boolean isJPA2_0() {
        return getJPAVersion() == JPAVersion.VERSION_2_0;
    }

    /**
     * Determines whether the JPA version defined by the JPQL grammar is 2.1.
     *
     * @return <code>true</code> if the JPQL grammar was defined for JPA 2.1; <code>false</code> if
     * it was defined for a more recent version
     */
    protected boolean isJPA2_1() {
        return getJPAVersion() == JPAVersion.VERSION_2_1;
    }

    /**
     * Determines whether the given subquery <code><b>SELECT</b></code> clause can return more than
     * one item or just a single. By default, only one item can be returned.
     *
     * @param expression The subquery <code><b>SELECT</b></code> clause
     * @return <code>true</code> if it can return more than one item; <code>false</code> if it needs
     * to return only one item
     */
    protected boolean isMultipleSubquerySelectItemsAllowed(SimpleSelectClause expression) {
        return false;
    }

    /**
     * Determines whether the JPA version for which the JPQL grammar was defined represents a version
     * that is newer than the given version.
     *
     * @param version The constant to verify if it's representing a version that is older than this one
     * @return <code>true</code> if this constant represents a newer version and the given constant
     * represents a version that is older; <code>false</code> if the given constant represents a
     * newer and this constant represents an older version
     */
    protected final boolean isNewerThan(JPAVersion version) {
        return getJPAVersion().isNewerThan(version);
    }

    /**
     * Determines whether the JPA version for which the JPQL grammar was defined represents a version
     * that is newer than the given version or if it's the same version.
     *
     * @param version The constant to verify if it's representing a version that is older than this
     * one or if it's the same than this one
     * @return <code>true</code> if this constant represents a newer version and the given constant
     * represents a version that is older or if it's the same constant; <code>false</code> if the
     * given constant represents a newer and this constant represents an older version
     */
    protected final boolean isNewerThanOrEqual(JPAVersion version) {
        return getJPAVersion().isNewerThanOrEqual(version);
    }

    /**
     * Determines whether the given sequence of characters is a numeric literal or not. There are
     * two types of numeric literal that is supported:
     * <ul>
     *    <li>Decimal literal</li>
     *    <li>Hexadecimal literal</li>
     * </ul>
     *
     * @param text The sequence of characters to validate
     * @return <code>true</code> if the given sequence of characters is a valid numeric literal;
     * <code>false</code> otherwise
     */
    protected boolean isNumericLiteral(String text) {

        // The ending 'l' or 'L' for a long number has to be removed, Java will not parse it
        if (text.endsWith("l") || text.endsWith("L")) {
            text = text.substring(0, text.length() - 1);
        }

        // Simply try to parse it as a double number (integer and hexadecimal are handled as well)
        try {
            Double.parseDouble(text);
            return true;
        }
        catch (Exception e2) {
            return false;
        }
    }

    /**
     * Determines whether the JPA version for which the JPQL grammar was defined represents a version
     * that is older than the given version.
     *
     * @param version The constant to verify if it's representing a version that is more recent
     * than this one
     * @return <code>true</code> if this constant represents an earlier version and the given
     * constant represents a version that is more recent; <code>false</code> if the given constant
     * represents an earlier version and this constant represents a more recent version
     */
    protected final boolean isOlderThan(JPAVersion version) {
        return getJPAVersion().isOlderThan(version);
    }

    /**
     * Determines whether the JPA version for which the JPQL grammar was defined represents a version
     * that is older than the given version or if it's the same version.
     *
     * @param version The constant to verify if it's representing a version that is more recent than
     * this one or if it's the same than this one
     * @return <code>true</code> if this constant represents an earlier version and the given
     * constant represents a version that is more recent or if it's the same constant; <code>false</code>
     * if the given constant represents an earlier version and this constant represents a more recent
     * version
     */
    protected final boolean isOlderThanOrEqual(JPAVersion version) {
        return getJPAVersion().isOlderThanOrEqual(version);
    }

    /**
     * Determines whether the given {@link Expression} is a child of the <b>WHERE</b> or <b>HAVING</b>
     * clause of the top-level query.
     *
     * @param expression The {@link Expression} to visit its parent hierarchy up to the clause
     * @return <code>true</code> if the first parent being a clause is the <b>WHERE</b> or <b>HAVING</b>
     * clause; <code>false</code> otherwise
     */
    protected boolean isOwnedByConditionalClause(Expression expression) {
        OwningClauseVisitor visitor = getOwningClauseVisitor();
        try {
            expression.accept(visitor);
            return visitor.whereClause  != null ||
                   visitor.havingClause != null;
        }
        finally {
            visitor.dispose();
        }
    }

    /**
     * Determines whether the given {@link Expression} is a child of the <b>FROM</b> clause of the
     * top-level query.
     *
     * @param expression The {@link Expression} to visit its parent hierarchy up to the clause
     * @return <code>true</code> if the first parent being a clause is the top-level <b>FROM</b>
     * clause; <code>false</code> otherwise
     */
    protected boolean isOwnedByFromClause(Expression expression) {
        OwningClauseVisitor visitor = getOwningClauseVisitor();
        try {
            expression.accept(visitor);
            return visitor.fromClause != null;
        }
        finally {
            visitor.dispose();
        }
    }

    /**
     * Determines whether the given {@link Expression} is a child of the <b>FROM</b> clause of a
     * subquery.
     *
     * @param expression The {@link Expression} to visit its parent hierarchy up to the clause
     * @return <code>true</code> if the first parent being a clause is the <b>FROM</b> clause of a
     * subquery; <code>false</code> otherwise
     */
    protected boolean isOwnedBySubFromClause(Expression expression) {
        OwningClauseVisitor visitor = getOwningClauseVisitor();
        try {
            expression.accept(visitor);
            return visitor.simpleFromClause != null;
        }
        finally {
            visitor.dispose();
        }
    }

    /**
     * Determines whether a subquery can be used in any clause of the top-level query.
     *
     * @return <code>true</code> if a subquery can be defined in any clause; <code>false</code> if
     * it can only be used within the <code>WHERE</code> and <code>HAVING</code> defined by the JPA
     * 1.0 and 2.0 specification document
     */
    protected abstract boolean isSubqueryAllowedAnywhere();

    /**
     * Determines whether the given variable is a valid Java identifier, which means it follows the
     * Java specification. The first letter has to be a Java identifier start and the others have to
     * be Java identifier parts.
     *
     * @param variable The variable to validate
     * @return <code>true</code> if the given variable follows the Java identifier specification;
     * <code>false</code> otherwise
     */
    protected boolean isValidJavaIdentifier(String variable) {

        for (int index = 0, count = variable.length(); index < count; index++) {

            int character = variable.charAt(index);

            if ((index == 0) && !Character.isJavaIdentifierStart(character)) {
                return false;
            }

            if ((index > 0) && !Character.isJavaIdentifierPart(character)) {
                return false;
            }
        }

        return true;
    }

    protected AbstractSingleEncapsulatedExpressionHelper<KeyExpression> keyExpressionHelper() {
        AbstractSingleEncapsulatedExpressionHelper<KeyExpression> helper = getHelper(KEY);
        if (helper == null) {
            helper = buildKeyExpressionHelper();
            registerHelper(KEY, helper);
        }
        return helper;
    }

    protected AbstractSingleEncapsulatedExpressionHelper<LengthExpression> lengthExpressionHelper() {
        AbstractSingleEncapsulatedExpressionHelper<LengthExpression> helper = getHelper(LENGTH);
        if (helper == null) {
            helper = buildLengthExpressionHelper();
            registerHelper(LENGTH, helper);
        }
        return helper;
    }

    protected AbstractTripleEncapsulatedExpressionHelper<LocateExpression> locateExpressionHelper() {
        AbstractTripleEncapsulatedExpressionHelper<LocateExpression> helper = getHelper(LOCATE);
        if (helper == null) {
            helper = buildLocateExpressionHelper();
            registerHelper(LOCATE, helper);
        }
        return helper;
    }

    protected AbstractSingleEncapsulatedExpressionHelper<LowerExpression> lowerExpressionHelper() {
        AbstractSingleEncapsulatedExpressionHelper<LowerExpression> helper = getHelper(LOWER);
        if (helper == null) {
            helper = buildLowerExpressionHelper();
            registerHelper(LOWER, helper);
        }
        return helper;
    }

    protected AbstractSingleEncapsulatedExpressionHelper<MaxFunction> maxFunctionHelper() {
        AbstractSingleEncapsulatedExpressionHelper<MaxFunction> helper = getHelper(MAX);
        if (helper == null) {
            helper = buildMaxFunctionHelper();
            registerHelper(MAX, helper);
        }
        return helper;
    }

    protected AbstractSingleEncapsulatedExpressionHelper<MinFunction> minFunctionHelper() {
        AbstractSingleEncapsulatedExpressionHelper<MinFunction> helper = getHelper(MIN);
        if (helper == null) {
            helper = buildMinFunctionHelper();
            registerHelper(MIN, helper);
        }
        return helper;
    }

    protected AbstractDoubleEncapsulatedExpressionHelper<ModExpression> modExpressionHelper() {
        AbstractDoubleEncapsulatedExpressionHelper<ModExpression> helper = getHelper(MOD);
        if (helper == null) {
            helper = buildModExpressionHelper();
            registerHelper(MOD, helper);
        }
        return helper;
    }

    protected AbstractDoubleEncapsulatedExpressionHelper<NullIfExpression> nullIfExpressionHelper() {
        AbstractDoubleEncapsulatedExpressionHelper<NullIfExpression> helper = getHelper(NULLIF);
        if (helper == null) {
            helper = buildNullIfExpressionHelper();
            registerHelper(NULLIF, helper);
        }
        return helper;
    }

    protected AbstractSingleEncapsulatedExpressionHelper<ObjectExpression> objectExpressionHelper() {
        AbstractSingleEncapsulatedExpressionHelper<ObjectExpression> helper = getHelper(OBJECT);
        if (helper == null) {
            helper = buildObjectExpressionHelper();
            registerHelper(OBJECT, helper);
        }
        return helper;
    }

    protected int position(Expression expression, int... extras) {
        int position = position(expression);
        for (int extra : extras) {
            position += extra;
        }
        return position;
    }

    /**
     * Registers the given helper.
     *
     * @param id The key used to cache the given helper
     * @param helper The helper to cache for future use
     */
    protected void registerHelper(String id, Object helper) {
        helpers.put(id, helper);
    }

    protected AbstractSingleEncapsulatedExpressionHelper<SizeExpression> sizeExpressionHelper() {
        AbstractSingleEncapsulatedExpressionHelper<SizeExpression> helper = getHelper(SIZE);
        if (helper == null) {
            helper = buildSizeExpressionHelper();
            registerHelper(SIZE, helper);
        }
        return helper;
    }

    protected AbstractSingleEncapsulatedExpressionHelper<SqrtExpression> sqrtExpressionHelper() {
        AbstractSingleEncapsulatedExpressionHelper<SqrtExpression> helper = getHelper(SQRT);
        if (helper == null) {
            helper = buildSqrtExpressionHelper();
            registerHelper(SQRT, helper);
        }
        return helper;
    }

    protected AbstractTripleEncapsulatedExpressionHelper<SubstringExpression> substringExpressionHelper() {
        AbstractTripleEncapsulatedExpressionHelper<SubstringExpression> helper = getHelper(SUBSTRING);
        if (helper == null) {
            helper = buildSubstringExpressionHelper();
            registerHelper(SUBSTRING, helper);
        }
        return helper;
    }

    protected AbstractSingleEncapsulatedExpressionHelper<SumFunction> sumFunctionHelper() {
        AbstractSingleEncapsulatedExpressionHelper<SumFunction> helper = getHelper(SUM);
        if (helper == null) {
            helper = buildSumFunctionHelper();
            registerHelper(SUM, helper);
        }
        return helper;
    }

    protected AbstractSingleEncapsulatedExpressionHelper<TrimExpression> trimExpressionHelper() {
        AbstractSingleEncapsulatedExpressionHelper<TrimExpression> helper = getHelper(TRIM);
        if (helper == null) {
            helper = buildTrimExpressionHelper();
            registerHelper(TRIM, helper);
        }
        return helper;
    }

    protected AbstractSingleEncapsulatedExpressionHelper<TypeExpression> typeExpressionHelper() {
        AbstractSingleEncapsulatedExpressionHelper<TypeExpression> helper = getHelper(TYPE);
        if (helper == null) {
            helper = buildTypeExpressionHelper();
            registerHelper(TYPE, helper);
        }
        return helper;
    }

    protected AbstractSingleEncapsulatedExpressionHelper<UpperExpression> upperExpressionHelper() {
        AbstractSingleEncapsulatedExpressionHelper<UpperExpression> helper = getHelper(UPPER);
        if (helper == null) {
            helper = buildUpperExpressionHelper();
            registerHelper(UPPER, helper);
        }
        return helper;
    }

    protected void validateAbstractConditionalClause(AbstractConditionalClause expression,
                                                     String missingConditionalExpressionMessageKey,
                                                     String invalidConditionalExpressionMessageKey) {

        // Missing conditional expression
        if (!expression.hasConditionalExpression()) {

            int startPosition = position(expression);
            int endPosition   = startPosition +
                                expression.getIdentifier().length() +
                                (expression.hasSpaceAfterIdentifier() ? 1 : 0);

            addProblem(expression, startPosition, endPosition, missingConditionalExpressionMessageKey);
        }
        else {
            Expression conditionalExpression = expression.getConditionalExpression();

            // Invalid conditional expression
            // Example: "... WHERE foo() = 1", right now it's parsed as 3 expressions
            if (getChildren(conditionalExpression).size() > 1) {
                int startPosition = position(conditionalExpression);
                int endPosition   = startPosition + length(conditionalExpression);
                addProblem(expression, startPosition, endPosition, invalidConditionalExpressionMessageKey);
            }
            else {
                // Invalid conditional expression
                if (!isValid(conditionalExpression, ConditionalExpressionBNF.ID)) {
                    int startPosition = position(conditionalExpression);
                    int endPosition   = startPosition + length(conditionalExpression);
                    addProblem(expression, startPosition, endPosition, invalidConditionalExpressionMessageKey);
                }

                // Visit the conditional expression
                conditionalExpression.accept(this);
            }
        }
    }

    /**
     * Validates the content of an {@link AbstractDoubleEncapsulatedExpression}, which encapsulates
     * two expressions separated by a comma.
     *
     * @param expression The {@link AbstractDoubleEncapsulatedExpression} to validate
     * @param helper This helper is used to retrieve specific information related to the {@link
     * AbstractDoubleEncapsulatedExpression expression} being validated
     */
    protected <T extends AbstractDoubleEncapsulatedExpression>
              void validateAbstractDoubleEncapsulatedExpression
              (T expression, AbstractDoubleEncapsulatedExpressionHelper<T> helper) {

        String identifier = helper.identifier(expression);

        // Missing '('
        if (!helper.hasLeftParenthesis(expression)) {
            int startPosition = position(expression) + identifier.length();
            addProblem(expression, startPosition, helper.leftParenthesisMissingKey(expression));
        }
        else {

            // Missing first expression
            if (!helper.hasFirstExpression(expression)) {

                int startPosition = position(expression) +
                                    identifier.length() +
                                    1 /* '(' */;

                addProblem(expression, startPosition, helper.firstExpressionMissingKey());
            }
            // Invalid first expression
            else if (!helper.isFirstExpressionValid(expression)) {

                int startPosition = position(expression) +
                                    identifier.length() +
                                    1 /* ( */;

                int endPosition = startPosition +
                                  helper.firstExpressionLength(expression);

                addProblem(expression, startPosition, endPosition, helper.firstExpressionInvalidKey());
            }
            else {
                expression.getFirstExpression().accept(this);
            }

            // Missing comma
            if (!helper.hasComma(expression)) {

                int startPosition = position(expression) +
                                    identifier.length() +
                                    1 /* ( */ +
                                    helper.firstExpressionLength(expression);

                addProblem(expression, startPosition, helper.missingCommaKey());
            }
            // After ','
            else {

                // Missing second expression
                if (!helper.hasSecondExpression(expression)) {

                    int startPosition = position(expression) +
                                        identifier.length() +
                                        1 /* ( */ +
                                        helper.firstExpressionLength(expression) +
                                        (expression.hasComma() ? 1 : 0) +
                                        (expression.hasSpaceAfterComma() ? 1 : 0);

                    addProblem(expression, startPosition, helper.secondExpressionMissingKey());
                }
                // Invalid second expression
                else if (!helper.isSecondExpressionValid(expression)) {

                    int startPosition = position(expression) +
                                        identifier.length() +
                                        1 /* ( */ +
                                        helper.firstExpressionLength(expression) +
                                        (expression.hasComma() ? 1 : 0) +
                                        (expression.hasSpaceAfterComma() ? 1 : 0);

                    int endPosition = startPosition + helper.secondExpressionLength(expression);

                    addProblem(expression, startPosition, endPosition, helper.secondExpressionInvalidKey());
                }
                else {
                    expression.getSecondExpression().accept(this);
                }
            }
        }

        // Missing ')'
        if (!helper.hasRightParenthesis(expression)) {

            int startPosition = position(expression) +
                                identifier.length() +
                                1 /* ( */ +
                                helper.firstExpressionLength(expression) +
                                (expression.hasComma() ? 1 : 0) +
                                (expression.hasSpaceAfterComma() ? 1 : 0) +
                                length(expression.getSecondExpression());

            addProblem(expression, startPosition, helper.rightParenthesisMissingKey(expression));
        }
    }

    protected void validateAbstractFromClause(AbstractFromClause expression) {

        // Missing declaration
        if (!expression.hasDeclaration()) {

            int startPosition = position(expression) +
                                4 /* FROM */ +
                                (expression.hasSpaceAfterFrom() ? 1 : 0);

            addProblem(expression, startPosition, AbstractFromClause_MissingIdentificationVariableDeclaration);
        }
        else {
            Expression declaration = expression.getDeclaration();

            // Two identification variable declarations have to be separated by a comma and
            // the FROM clause cannot end with a comma
            validateCollectionSeparatedByComma(
                declaration,
                AbstractFromClause_IdentificationVariableDeclarationEndsWithComma,
                AbstractFromClause_IdentificationVariableDeclarationIsMissingComma
            );

            // Validate the declaration
            declaration.accept(this);
        }
    }

    /**
     * Validates the select expression of the given <code>SELECT</code> clause. The select expression
     * will only be visited if its JPQL query BNF is part of the select item BNF.
     *
     * @param expression The {@link AbstractSelectClause} to validate
     * @param multipleSelectItemsAllowed Determines whether the <code><b>SELECT</b></code> can have
     * one or more select expression or not
     */
    protected void validateAbstractSelectClause(AbstractSelectClause expression,
                                                boolean multipleSelectItemsAllowed) {

        // Missing select expression
        if (!expression.hasSelectExpression()) {

            int startPosition = position(expression) +
                                6 /* SELECT */ +
                                (expression.hasSpaceAfterSelect() ? 1 : 0) +
                                (expression.hasDistinct() ? 8 : 0) +
                                (expression.hasSpaceAfterDistinct() ? 1 : 0);

            addProblem(expression, startPosition, AbstractSelectClause_MissingSelectExpression);
        }
        else {
            Expression selectExpression = expression.getSelectExpression();

            // Check for collection expression first
            if (isCollectionExpression(selectExpression)) {

                // The SELECT clause does not support a collection of select expressions
                if (!multipleSelectItemsAllowed) {

                    int startPosition = position(expression) +
                                        6 /* SELECT */ +
                                        (expression.hasSpaceAfterSelect() ? 1 : 0) +
                                        (expression.hasDistinct() ? 8 : 0) +
                                        (expression.hasSpaceAfterDistinct() ? 1 : 0);

                    int endPosition = startPosition + length(selectExpression);

                    addProblem(selectExpression, startPosition, endPosition, SimpleSelectClause_NotSingleExpression);
                }
                // Visit the select expression
                else {
                    selectExpression.accept(this);
                }
            }
            // The select expression is not valid
            else if (!isValid(selectExpression, expression.getSelectItemQueryBNFId())) {

                int startPosition = position(expression) +
                                    6 /* SELECT */ +
                                    (expression.hasSpaceAfterSelect() ? 1 : 0) +
                                    (expression.hasDistinct() ? 8 : 0) +
                                    (expression.hasSpaceAfterDistinct() ? 1 : 0);

                int endPosition = startPosition + length(selectExpression);

                addProblem(expression, startPosition, endPosition, AbstractSelectClause_InvalidSelectExpression);
            }
            // Visit the select expression
            else {
                selectExpression.accept(this);
            }
        }
    }

    protected void validateAbstractSelectStatement(AbstractSelectStatement expression) {

        // Does not have a FROM clause
        if (!expression.hasFromClause()) {

            int startPosition = position(expression) +
                                length(expression.getSelectClause()) +
                                (expression.hasSpaceAfterSelect() ? 1 : 0);

            addProblem(expression, startPosition, AbstractSelectStatement_FromClauseMissing);
        }
    }

    protected <T extends AbstractSingleEncapsulatedExpression>
              void validateAbstractSingleEncapsulatedExpression
              (T expression, AbstractSingleEncapsulatedExpressionHelper<T> helper) {

        String identifier = helper.identifier(expression);

        // Missing '('
        if (!helper.hasLeftParenthesis(expression)) {
            int startPosition = position(expression) + identifier.length();

            addProblem(
                expression,
                startPosition,
                helper.leftParenthesisMissingKey(expression),
                helper.arguments(expression)
            );
        }
        // Missing encapsulated expression
        else if (helper.isEncapsulatedExpressionMissing(expression)) {

            int startPosition = position(expression) +
                                identifier.length()  +
                                1 /* '(' */          +
                                helper.lengthBeforeEncapsulatedExpression(expression);

            addProblem(
                expression,
                startPosition,
                helper.encapsulatedExpressionMissingKey(expression),
                helper.arguments(expression)
            );
        }
        // Validate the encapsulated expression
        else {
            // The encapsulated expression is not valid
            if (!helper.isEncapsulatedExpressionValid(expression)) {
                int startPosition = position(expression) +
                                    identifier.length()  +
                                    1 /* '(' */          +
                                    helper.lengthBeforeEncapsulatedExpression(expression);

                int endPosition = startPosition + helper.encapsulatedExpressionLength(expression);

                addProblem(
                    expression,
                    startPosition,
                    endPosition,
                    helper.encapsulatedExpressionInvalidKey(expression),
                    helper.arguments(expression)
                );
            }
            // Now visit the encapsulated expression
            else {
                super.visit(expression);
            }
        }

        // Missing ')'
        if (!helper.hasRightParenthesis(expression)) {

            int startPosition = position(expression) + length(expression);

            addProblem(
                expression,
                startPosition,
                helper.rightParenthesisMissingKey(expression),
                helper.arguments(expression)
            );
        }
    }

    protected <T extends AbstractTripleEncapsulatedExpression>
              void validateAbstractTripleEncapsulatedExpression
              (T expression, AbstractTripleEncapsulatedExpressionHelper<T> helper) {

        String identifier = helper.identifier(expression);

        // Missing '('
        if (!helper.hasLeftParenthesis(expression)) {
            int startPosition = position(expression) + identifier.length();
            addProblem(expression, startPosition, helper.leftParenthesisMissingKey(expression));
        }
        else {

            // Missing first expression
            if (!helper.hasFirstExpression(expression)) {

                int startPosition = position(expression) +
                                    identifier.length() +
                                    1 /* ( */;

                addProblem(expression, startPosition, helper.firstExpressionMissingKey());
            }
            // Invalid first expression
            else if (!helper.isFirstExpressionValid(expression)) {

                int startPosition = position(expression) +
                                    identifier.length() +
                                    1 /* ( */;

                int endPosition = startPosition + helper.firstExpressionLength(expression);

                addProblem(expression, startPosition, endPosition, helper.firstExpressionInvalidKey());
            }
            else {
                expression.getFirstExpression().accept(this);
            }

            // Missing first comma
            if (helper.hasFirstExpression(expression) &&
                !expression.hasFirstComma()) {

                int startPosition = position(expression) +
                                    identifier.length() +
                                    1 /* ( */ +
                                    helper.firstExpressionLength(expression);

                addProblem(expression, startPosition, helper.firstCommaMissingKey());
            }

            // Validate second expression
            if (expression.hasFirstComma()) {

                // Missing second expression
                if (!helper.hasSecondExpression(expression)) {

                    int startPosition = position(expression) +
                                        identifier.length() +
                                        1 /* ( */ +
                                        helper.firstExpressionLength(expression) +
                                        (expression.hasFirstComma() ? 1 : 0) +
                                        (expression.hasSpaceAfterFirstComma() ? 1 : 0);

                    addProblem(expression, startPosition, helper.secondExpressionMissingKey());
                }
                // Invalid second expression
                else if (!helper.isSecondExpressionValid(expression)) {

                    int startPosition = position(expression) +
                                        identifier.length() +
                                        1 /* ( */ +
                                        helper.firstExpressionLength(expression) +
                                        (expression.hasFirstComma() ? 1 : 0) +
                                        (expression.hasSpaceAfterFirstComma() ? 1 : 0);

                    int endPosition = startPosition + helper.secondExpressionLength(expression);

                    addProblem(expression, startPosition, endPosition, helper.secondExpressionInvalidKey());
                }
                else {
                    expression.getSecondExpression().accept(this);
                }
            }

            // Missing second comma
            if (helper.hasSecondExpression(expression) &&
                !expression.hasSecondComma() &&
                helper.hasThirdExpression(expression)) {

                int startPosition = position(expression) +
                                    identifier.length() +
                                    1 /* ( */ +
                                    helper.firstExpressionLength(expression) +
                                    (expression.hasFirstComma() ? 1 : 0) +
                                    (expression.hasSpaceAfterFirstComma() ? 1 : 0) +
                                    helper.secondExpressionLength(expression);

                addProblem(expression, startPosition, helper.secondCommaMissingKey());
            }

            // Validate third expression
            if (expression.hasSecondComma()) {

                // Missing third expression
                if (!helper.hasThirdExpression(expression)) {

                    int startPosition = position(expression) +
                                        identifier.length() +
                                        1 /* ( */ +
                                        helper.firstExpressionLength(expression) +
                                        (expression.hasFirstComma() ? 1 : 0) +
                                        (expression.hasSpaceAfterFirstComma() ? 1 : 0) +
                                        helper.secondExpressionLength(expression) +
                                        (expression.hasSecondComma() ? 1 : 0) +
                                        (expression.hasSpaceAfterSecondComma() ? 1 : 0);

                    addProblem(expression, startPosition, helper.thirdExpressionMissingKey());
                }
                // Invalid third expression
                else if (!helper.isThirdExpressionValid(expression)) {

                    int startPosition = position(expression) +
                                        identifier.length() +
                                        1 /* ( */ +
                                        helper.firstExpressionLength(expression) +
                                        (expression.hasFirstComma() ? 1 : 0) +
                                        (expression.hasSpaceAfterFirstComma() ? 1 : 0) +
                                        helper.secondExpressionLength(expression) +
                                        (expression.hasSecondComma() ? 1 : 0) +
                                        (expression.hasSpaceAfterSecondComma() ? 1 : 0);

                    int endPosition = startPosition + helper.thirdExpressionLength(expression);

                    addProblem(expression, startPosition, endPosition, helper.thirdExpressionInvalidKey());
                }
                else {
                    expression.getThirdExpression().accept(this);
                }
            }
        }

        // Missing ')'
        if (!helper.hasRightParenthesis(expression)) {

            int startPosition = position(expression) +
                                identifier.length() +
                                1 /* ( */ +
                                helper.firstExpressionLength(expression) +
                                (expression.hasFirstComma() ? 1 : 0) +
                                (expression.hasSpaceAfterFirstComma() ? 1 : 0) +
                                helper.secondExpressionLength(expression) +
                                (expression.hasSecondComma() ? 1 : 0) +
                                (expression.hasSpaceAfterSecondComma() ? 1 : 0) +
                                helper.thirdExpressionLength(expression);

            addProblem(expression, startPosition, helper.rightParenthesisMissingKey(expression));
        }
    }

    protected <T extends AggregateFunction> void validateAggregateFunctionLocation
        (T expression, AbstractSingleEncapsulatedExpressionHelper<T> helper) {

        // Check to see if the aggregate function is in the right clause
        OwningClauseVisitor visitor = getOwningClauseVisitor();
        boolean valid = true;
        try {
            expression.accept(visitor);
            valid = visitor.selectClause       != null ||
                    visitor.simpleSelectClause != null ||
                    visitor.groupByClause      != null ||
                    visitor.orderByClause      != null ||
                    visitor.havingClause       != null;
        }
        finally {
            visitor.dispose();
        }

        // The function is used in an invalid clause
        if (!valid) {

            int startPosition = position(expression);
            int endPosition   = startPosition + length(expression);

            addProblem(
                expression,
                startPosition,
                endPosition,
                AggregateFunction_WrongClause,
                expression.getIdentifier());
        }
        else {
            validateAbstractSingleEncapsulatedExpression(expression, helper);
        }
    }

    protected void validateArithmeticExpression(ArithmeticExpression expression) {
        validateCompoundExpression(
            expression,
            expression.getArithmeticSign(),
            ArithmeticExpression_MissingLeftExpression,
            ArithmeticExpression_InvalidLeftExpression,
            ArithmeticExpression_MissingRightExpression,
            ArithmeticExpression_InvalidRightExpression,
            ArithmeticExpressionBNF.ID,
            ArithmeticTermBNF.ID
        );
    }

    /**
     * Validates the given {@link Expression} by making sure each child is separated by a comma.
     *
     * @param expression The {@link Expression} to validate its children, which should be a series
     * of {@link Expression} separated by a comma
     * @param endsWithCommaProblemKey The problem key describing the {@link CollectionExpression} is
     * ending with a comma
     * @param missingCommaProblemKey The problem key describing the {@link CollectionExpression} has
     * two items not separated by a comma
     */
    protected void validateCollectionSeparatedByComma(Expression expression,
                                                      String endsWithCommaProblemKey,
                                                      String missingCommaProblemKey) {

        CollectionSeparatedByCommaValidator validator = collectionSeparatedByCommaValidator();

        try {
            validator.endsWithCommaProblemKey  = endsWithCommaProblemKey;
            validator.wrongSeparatorProblemKey = missingCommaProblemKey;
            expression.accept(validator);
        }
        finally {
            validator.endsWithCommaProblemKey  = null;
            validator.wrongSeparatorProblemKey = null;
        }
    }

    /**
     * Validates the given {@link Expression} by making sure each child is separated by a whitespace.
     *
     * @param expression The {@link Expression} to validate its children, which should be a series
     * of {@link Expression} separated by a whitespace
     * @param endsWithCommaProblemKey The problem key describing the {@link CollectionExpression}
     * is ending with a comma
     * @param hasCommaProblemKey The problem key describing the {@link CollectionExpression} has two
     * items separated by a comma
     */
    protected void validateCollectionSeparatedBySpace(Expression expression,
                                                      String endsWithCommaProblemKey,
                                                      String hasCommaProblemKey) {

        CollectionSeparatedBySpaceValidator validator = collectionSeparatedBySpaceValidator();

        try {
            validator.endsWithCommaProblemKey  = endsWithCommaProblemKey;
            validator.wrongSeparatorProblemKey = hasCommaProblemKey;
            expression.accept(validator);
        }
        finally {
            validator.endsWithCommaProblemKey  = null;
            validator.wrongSeparatorProblemKey = null;
        }
    }

    protected void validateCompoundExpression(CompoundExpression expression,
                                              String identifier,
                                              String missingLeftExpression,
                                              String invalidLeftExpression,
                                              String missingRightExpression,
                                              String invalidRightExpression,
                                              String leftExpressionQueryBNF,
                                              String rightExpressionQueryBNF) {

        // Missing left expression
        if (!expression.hasLeftExpression()) {
            int startPosition = position(expression);
            addProblem(expression, startPosition, missingLeftExpression);
        }
        else {
            Expression leftExpression = expression.getLeftExpression();

            // Invalid left expression
            if (!isValid(leftExpression, leftExpressionQueryBNF)) {

                int startPosition = position(expression);
                int endPosition   = startPosition + length(leftExpression);
                addProblem(expression, startPosition, endPosition, invalidLeftExpression);
            }
            else {
                leftExpression.accept(this);
            }
        }

        // Missing right expression
        if (!expression.hasRightExpression()) {

            int startPosition = position(expression) +
                                length(expression.getLeftExpression()) +
                                (expression.hasLeftExpression() ? 1 : 0) +
                                identifier.length() +
                                (expression.hasSpaceAfterIdentifier() ? 1 : 0);

            addProblem(expression, startPosition, missingRightExpression);
        }
        else {
            Expression rightExpression = expression.getRightExpression();

            // Invalid right expression
            if (!isValid(rightExpression, rightExpressionQueryBNF)) {

                int startPosition = position(expression) +
                                    length(expression.getLeftExpression()) +
                                    (expression.hasLeftExpression() ? 1 : 0) +
                                    identifier.length() +
                                    (expression.hasSpaceAfterIdentifier() ? 1 : 0);

                int endPosition = startPosition + length(rightExpression);

                addProblem(expression, startPosition, endPosition, invalidRightExpression);
            }
            else {
                rightExpression.accept(this);
            }
        }
    }

    protected void validateIdentificationVariableDeclaration(IdentificationVariableDeclaration expression) {

        // The range variable declaration is missing
        if (!expression.hasRangeVariableDeclaration()) {
            addProblem(expression, position(expression), IdentificationVariableDeclaration_MissingRangeVariableDeclaration);
        }
        else {
            expression.getRangeVariableDeclaration().accept(this);
        }

        validateJoins(expression);
    }

    /**
     * Validates the given variable name to make sure:
     * <ul>
     * <li>It is not a JPQL reserved identifier;</li>
     * <li>It is a valid Java identifier.</li>
     * </ul>
     *
     * @param expression The expression to validate
     * @param variableName The text to actually validate
     * @param variableLength The actual length of the text, which can be longer than the text that is
     * validated
     * @param reservedWordProblemKey The problem key used when the variable name is a reserved JPQL
     * identifier
     * @param invalidJavaIdentifierProblemKey The problem key used when the variable name is not a
     * valid Java identifier
     */
    protected void validateIdentifier(Expression expression,
                                      String variableName,
                                      int variableLength,
                                      String reservedWordProblemKey,
                                      String invalidJavaIdentifierProblemKey) {

        // Must not be a reserved identifier. An exception is ORDER and GROUP because the actual
        // identifiers are ORDER BY and GROUP BY
        if (getExpressionRegistry().isIdentifier(variableName) &&
            !"ORDER".equalsIgnoreCase(variableName) &&
            !"GROUP".equalsIgnoreCase(variableName)) {

            int startPosition = position(expression);
            int endPosition   = startPosition + variableLength;
            addProblem(expression, startPosition, endPosition, reservedWordProblemKey, variableName);
        }
        // The character sequence must begin with a Java identifier start character, and all other
        // characters must be Java identifier part characters. An identifier start character is any
        // character for which the method Character.isJavaIdentifierStart returns true. This includes
        // the underscore (_) character and the dollar sign ($) character. An identifier part
        // character is any character for which the method Character.isJavaIdentifierPart returns
        // true. The question mark (?) character is reserved for use by the Java Persistence query
        // language. An identification variable must not be a reserved identifier or have the same
        // name as any entity in the same persistence unit
        else if (!isValidJavaIdentifier(variableName)) {
            int startPosition = position(expression);
            int endPosition   = startPosition + variableLength;
            addProblem(expression, startPosition, endPosition, invalidJavaIdentifierProblemKey, variableName);
        }
    }

    protected void validateInputParameters(JPQLExpression expression) {

        int positionalCount = 0;
        int namedCount = 0;

        for (InputParameter inputParameter : inputParameters) {
            if (inputParameter.isNamed()) {
                namedCount++;
            }
            else if (inputParameter.isPositional()) {
                positionalCount++;
            }
        }

        if ((positionalCount > 0) && (namedCount > 0)) {
            for (InputParameter parameter : inputParameters) {
                addProblem(parameter, InputParameter_Mixture);
            }
        }
    }

    protected void validateJoins(IdentificationVariableDeclaration expression) {

        // Validate the JOIN expressions
        if (expression.hasJoins()) {

            Expression joins = expression.getJoins();
            List<Expression> children = getChildren(joins);

            // Validate multiple JOIN expression
            if (children.size() > 1) {

                validateCollectionSeparatedBySpace(
                    joins,
                    IdentificationVariableDeclaration_JoinsEndWithComma,
                    IdentificationVariableDeclaration_JoinsHaveComma
                );

                // Make sure each child is a JOIN expression
                for (int index = children.size(); --index >= 0; ) {
                    Expression child = children.get(index);
                    child.accept(this);
                }
            }
            // Make sure the single expression is a JOIN expression
            // Validate the JOIN expression
            else {
                joins.accept(this);
            }
        }
    }

    protected void validateLikeExpressionEscapeCharacter(LikeExpression expression) {

        Expression escapeCharacter = expression.getEscapeCharacter();

        // Check for a string literal (single quoted character)
        String character = literal(escapeCharacter, LiteralType.STRING_LITERAL);

        // Check for a single character
        if ((character.length() > 0) && ExpressionTools.isQuote(character.charAt(0))) {

            // Unquote the literal first
            character = ExpressionTools.unquote(character);

            // The escape character is not a single character literal
            if (character.length() != 1) {

                int startPosition = position(expression) +
                                    length(expression.getStringExpression()) +
                                    (expression.hasSpaceAfterStringExpression() ? 1 : 0) +
                                    (expression.hasNot() ? 4 : 0) +
                                    4 /* LIKE */ +
                                    (expression.hasSpaceAfterLike() ? 1 : 0) +
                                    length(expression.getPatternValue()) +
                                    (expression.hasSpaceAfterPatternValue() ? 1 : 0) +
                                    6 + /* ESCAPE */ +
                                    (expression.hasSpaceAfterEscape() ? 1 : 0);

                int endPosition = startPosition +  length(escapeCharacter);

                addProblem(
                    expression,
                    startPosition,
                    endPosition,
                    LikeExpression_InvalidEscapeCharacter,
                    escapeCharacter.toActualText()
                );
            }
        }
        else {
            // Check for an input parameter
            character = literal(escapeCharacter, LiteralType.INPUT_PARAMETER);

            if ((character.length() == 0) &&
                !isValid(escapeCharacter, LikeExpressionEscapeCharacterBNF.ID)) {

                int startPosition = position(expression) +
                                    length(expression.getStringExpression()) +
                                    4 /* LIKE */ +
                                    (expression.hasSpaceAfterStringExpression() ? 1 : 0) +
                                    (expression.hasNot() ? 1 : 0) +
                                    (expression.hasSpaceAfterLike() ? 1 : 0) +
                                    length(expression.getPatternValue()) +
                                    (expression.hasSpaceAfterPatternValue() ? 1 : 0) +
                                    6 + /* ESCAPE */ +
                                    (expression.hasSpaceAfterEscape() ? 1 : 0);

                int endPosition = startPosition + length(escapeCharacter);

                addProblem(
                    expression,
                    startPosition,
                    endPosition,
                    LikeExpression_InvalidEscapeCharacter,
                    escapeCharacter.toActualText()
                );
            }
        }
    }

    protected void validateLogicalExpression(LogicalExpression expression,
                                             String leftExpressionQueryBNF,
                                             String rightExpressionQueryBNF) {

        validateCompoundExpression(
            expression,
            expression.getIdentifier(),
            LogicalExpression_MissingLeftExpression,
            LogicalExpression_InvalidLeftExpression,
            LogicalExpression_MissingRightExpression,
            LogicalExpression_InvalidRightExpression,
            leftExpressionQueryBNF,
            rightExpressionQueryBNF
        );
    }

    protected void validateOwningClause(InputParameter expression, String parameter) {

        if (!isInputParameterInValidLocation(expression)) {
            int startPosition = position(expression);
            int endPosition   = startPosition + parameter.length();
            addProblem(expression, startPosition, endPosition, InputParameter_WrongClauseDeclaration);
        }
    }

    protected void validatePathExpression(AbstractPathExpression expression) {

        // Missing identification variable
        if (!expression.hasIdentificationVariable() &&
            !expression.hasVirtualIdentificationVariable()) {

            addProblem(expression, AbstractPathExpression_MissingIdentificationVariable);
        }
        // Validate the identification variable
        else {
            expression.getIdentificationVariable().accept(this);
        }

        // Cannot end with a dot
        if (expression.endsWithDot()) {
            addProblem(expression, AbstractPathExpression_CannotEndWithComma);
        }
    }

    protected void validateSimpleSelectStatement(SimpleSelectStatement expression) {
        validateAbstractSelectStatement(expression);
    }

    protected AbstractSingleEncapsulatedExpressionHelper<ValueExpression> valueExpressionHelper() {
        AbstractSingleEncapsulatedExpressionHelper<ValueExpression> helper = getHelper(VALUE);
        if (helper == null) {
            helper = buildValueExpressionHelper();
            registerHelper(VALUE, helper);
        }
        return helper;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(AbsExpression expression) {
        validateAbstractSingleEncapsulatedExpression(expression, absExpressionHelper());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(AbstractSchemaName expression) {
        // Nothing to validate
        super.visit(expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(AdditionExpression expression) {
        validateArithmeticExpression(expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(AllOrAnyExpression expression) {

        validateAbstractSingleEncapsulatedExpression(expression, allOrAnyExpressionHelper());

        // Make sure the expression is part of a comparison expression
        if (!isChildOfComparisonExpession(expression)) {
            addProblem(expression, AllOrAnyExpression_NotPartOfComparisonExpression);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(AndExpression expression) {
        validateLogicalExpression(expression, ConditionalExpressionBNF.ID, ConditionalExpressionBNF.ID);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(ArithmeticFactor expression) {

        // Missing expression after +/-
        if (!expression.hasExpression()) {
            int startPosition = position(expression) + 1;
            addProblem(expression, startPosition, ArithmeticFactor_MissingExpression);
        }
        else {
            Expression arithmeticExpression = expression.getExpression();

            if (!isValid(arithmeticExpression, ArithmeticPrimaryBNF.ID)) {
                int startIndex = position(expression) + 1;
                int endIndex   = startIndex;
                addProblem(expression, startIndex, endIndex, ArithmeticFactor_InvalidExpression);
            }
            else {
                arithmeticExpression.accept(this);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(AvgFunction expression) {
        validateAggregateFunctionLocation(expression, avgFunctionHelper());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(BadExpression expression) {
        int startPosition = position(expression);
        int endPosition   = startPosition + length(expression);
        addProblem(expression, startPosition, endPosition, BadExpression_InvalidExpression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(BetweenExpression expression) {

        // Missing expression
        if (!expression.hasExpression()) {
            int startPosition = position(expression);
            addProblem(expression, startPosition, BetweenExpression_MissingExpression);
        }

        // Missing lower bound expression
        if (!expression.hasLowerBoundExpression()) {

            int startPosition = position(expression) +
                                length(expression.getExpression()) +
                                (expression.hasExpression() ? 1 : 0) +
                                (expression.hasNot() ? 11 /* NOT BETWEEN */ : 7 /* BETWEEN */) +
                                (expression.hasSpaceAfterBetween() ? 1 : 0);

            addProblem(expression, startPosition, BetweenExpression_MissingLowerBoundExpression);
        }
        // Missing 'AND'
        else if (!expression.hasAnd()) {

            int startPosition = position(expression) +
                                length(expression.getExpression()) +
                                (expression.hasExpression() ? 1 : 0) +
                                (expression.hasNot() ? 11 /* NOT BETWEEN */ : 7 /* BETWEEN */) +
                                (expression.hasSpaceAfterBetween() ? 1 : 0) +
                                length(expression.getLowerBoundExpression()) +
                                (expression.hasSpaceAfterLowerBound() ? 1 : 0);

            addProblem(expression, startPosition, BetweenExpression_MissingAnd);
        }
        // Missing upper bound expression
        else if (!expression.hasUpperBoundExpression()) {

            int startPosition = position(expression) +
                                length(expression.getExpression()) +
                                (expression.hasExpression() ? 1 : 0) +
                                (expression.hasNot() ? 11 /* NOT BETWEEN */ : 7 /* BETWEEN */) +
                                (expression.hasSpaceAfterBetween() ? 1 : 0) +
                                length(expression.getLowerBoundExpression()) +
                                (expression.hasSpaceAfterLowerBound() ? 1 : 0) +
                                3 /* AND */ +
                                (expression.hasSpaceAfterAnd() ? 1 : 0);

            addProblem(expression, startPosition, BetweenExpression_MissingUpperBoundExpression);
        }

        super.visit(expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(CaseExpression expression) {

        // JPA 1.0 does not support a CASE expression
        if (isJPA1_0()) {
            addProblem(expression, CaseExpression_InvalidJPAVersion);
        }
        else {
            // WHEN clauses can't be separated by commas
            if (expression.hasWhenClauses()) {
                validateCollectionSeparatedBySpace(
                    expression.getWhenClauses(),
                    CaseExpression_WhenClausesEndWithComma,
                    CaseExpression_WhenClausesHasComma
                );
            }
            // At least one WHEN clause must be specified
            else {

                int startPosition = position(expression) +
                                    4 /* CASE */ +
                                    (expression.hasSpaceAfterCase() ? 1 : 0) +
                                    length(expression.getCaseOperand()) +
                                    (expression.hasSpaceAfterCaseOperand() ? 1 : 0);

                addProblem(expression, startPosition, CaseExpression_MissingWhenClause);
            }

            // Missing ELSE
            if (expression.hasWhenClauses() &&
               !expression.hasElse()) {

                int startPosition = position(expression) +
                                    4 /* CASE */ +
                                    (expression.hasSpaceAfterCase() ? 1 : 0) +
                                    length(expression.getCaseOperand()) +
                                    (expression.hasSpaceAfterCaseOperand() ? 1 : 0) +
                                    length(expression.getWhenClauses()) +
                                    (expression.hasSpaceAfterWhenClauses() ? 1 : 0);

                addProblem(expression, startPosition, CaseExpression_MissingElseIdentifier);
            }
            // Missing ELSE expression
            else if (expression.hasElse() &&
                    !expression.hasElseExpression()) {

                int startPosition = position(expression) +
                                    4 /* CASE */ +
                                    (expression.hasSpaceAfterCase() ? 1 : 0) +
                                    length(expression.getCaseOperand()) +
                                    (expression.hasSpaceAfterCaseOperand() ? 1 : 0) +
                                    length(expression.getWhenClauses()) +
                                    (expression.hasSpaceAfterWhenClauses() ? 1 : 0) +
                                    4 /* ELSE */ +
                                    (expression.hasSpaceAfterElse() ? 1 : 0);

                addProblem(expression, startPosition, CaseExpression_MissingElseExpression);
            }
            // Missing END
            else if (expression.hasElseExpression() &&
                    !expression.hasEnd()) {

                int startPosition = position(expression) +
                                    4 /* CASE */ +
                                    (expression.hasSpaceAfterCase() ? 1 : 0) +
                                    length(expression.getCaseOperand()) +
                                    (expression.hasSpaceAfterCaseOperand() ? 1 : 0) +
                                    length(expression.getWhenClauses()) +
                                    (expression.hasSpaceAfterWhenClauses() ? 1 : 0) +
                                    4 /* ELSE */ +
                                    (expression.hasSpaceAfterElse() ? 1 : 0) +
                                    length(expression.getElseExpression()) +
                                    (expression.hasSpaceAfterElseExpression() ? 1 : 0);

                addProblem(expression, startPosition, CaseExpression_MissingEndIdentifier);
            }

            super.visit(expression);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(CoalesceExpression expression) {

        // JPA 1.0 does not support a COALESCE expression
        if (isJPA1_0()) {
            addProblem(expression, CoalesceExpression_InvalidJPAVersion);
        }
        else {
            validateAbstractSingleEncapsulatedExpression(expression, coalesceExpressionHelper());
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(CollectionExpression expression) {
        // Nothing to validate, it's done by the parent expression
        // but we want to validate its children
        super.visit(expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(CollectionMemberDeclaration expression) {

        // FROM => 'IN (x) AS y'
        if (isOwnedByFromClause(expression)) {

            // Missing '('
            if (!expression.hasLeftParenthesis()) {
                int startPosition = position(expression) + 2 /* IN */;
                addProblem(expression, startPosition, CollectionMemberDeclaration_MissingLeftParenthesis);
            }
            // Missing collection valued path expression
            else if (!expression.hasCollectionValuedPathExpression()) {
                int startPosition = position(expression) + 3 /* IN( */;
                addProblem(expression, startPosition, CollectionMemberDeclaration_MissingCollectionValuedPathExpression);
            }
            // Missing right parenthesis
            else if (!expression.hasRightParenthesis()) {

                int startPosition = position(expression) +
                                    2 /* IN */ +
                                    (expression.hasLeftParenthesis() ? 1 : 0) +
                                    (expression.hasSpaceAfterIn() ? 1 : 0) +
                                    length(expression.getCollectionValuedPathExpression());

                addProblem(expression, startPosition, CollectionMemberDeclaration_MissingRightParenthesis);
            }

            // Missing identification variable
            if (expression.hasRightParenthesis() &&
               !expression.hasIdentificationVariable()) {

                int startPosition = position(expression) +
                                    2 /* IN */ +
                                    (expression.hasLeftParenthesis() ? 1 : 0) +
                                    (expression.hasSpaceAfterIn() ? 1 : 0) +
                                    length(expression.getCollectionValuedPathExpression()) +
                                    1 /* ')' */ +
                                    (expression.hasSpaceAfterRightParenthesis() ? 1 : 0) +
                                    (expression.hasAs() ? 2 : 0) +
                                    (expression.hasSpaceAfterAs() ? 1 : 0);

                addProblem(expression, startPosition, CollectionMemberDeclaration_MissingIdentificationVariable);
            }
        }
        // Subquery FROM => 'IN (x) AS y' or 'IN x'
        else {

            // Missing '('
            if (!expression.hasLeftParenthesis() &&
                 expression.hasRightParenthesis()) {

                int startPosition = position(expression) + 2; // IN
                addProblem(expression, startPosition, CollectionMemberDeclaration_MissingLeftParenthesis);
            }
            // Missing collection valued path expression
            else if (!expression.hasCollectionValuedPathExpression()) {

                int startPosition = position(expression) +
                                    2 /* IN */ +
                                    (expression.hasSpaceAfterIn() ? 1 : 0);

                addProblem(expression, startPosition, CollectionMemberDeclaration_MissingCollectionValuedPathExpression);
            }
            // Missing right parenthesis
            else if (expression.hasLeftParenthesis() &&
                    !expression.hasRightParenthesis()) {

                int startPosition = position(expression) +
                                    2 /* IN */ +
                                    (expression.hasLeftParenthesis() ? 1 : 0) +
                                    (expression.hasSpaceAfterIn() ? 1 : 0) +
                                    length(expression.getCollectionValuedPathExpression());

                addProblem(expression, startPosition, CollectionMemberDeclaration_MissingRightParenthesis);
            }

            // Missing identification variable
            if (expression.hasRightParenthesis() &&
               !expression.hasIdentificationVariable()) {

                int startPosition = position(expression) +
                                    2 /* IN */ +
                                    (expression.hasLeftParenthesis() ? 1 : 0) +
                                    (expression.hasSpaceAfterIn() ? 1 : 0) +
                                    length(expression.getCollectionValuedPathExpression()) +
                                    1 /* ')' */ +
                                    (expression.hasSpaceAfterRightParenthesis() ? 1 : 0) +
                                    (expression.hasAs() ? 2 : 0) +
                                    (expression.hasSpaceAfterAs() ? 1 : 0);

                addProblem(expression, startPosition, CollectionMemberDeclaration_MissingIdentificationVariable);
            }
        }

        super.visit(expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(CollectionMemberExpression expression) {

        // Missing entity expression
        if (!expression.hasEntityExpression()) {
            int startPosition = position(expression);
            addProblem(expression, startPosition, CollectionMemberExpression_MissingEntityExpression);
        }

        // Missing collection valued path expression
        if (!expression.hasCollectionValuedPathExpression()) {

            int startPosition = position(expression) +
                                length(expression.getEntityExpression()) +
                                (expression.hasEntityExpression() ? 1 : 0) +
                                (expression.hasNot() ? 4 /* NOT + whitespace */ : 0) +
                                6 /* MEMBER */ +
                                (expression.hasSpaceAfterMember() ? 1 : 0) +
                                (expression.hasOf() ? 2 : 0) +
                                (expression.hasSpaceAfterOf() ? 1 : 0);

            addProblem(expression, startPosition, CollectionMemberExpression_MissingCollectionValuedPathExpression);
        }
        else {
            Expression pathExpression = expression.getCollectionValuedPathExpression();

            // The expression is not a path expression
            if (!isValid(pathExpression, CollectionValuedPathExpressionBNF.ID)) {

                int startPosition = position(expression) +
                                    length(expression.getEntityExpression()) +
                                    (expression.hasEntityExpression() ? 1 : 0) +
                                    (expression.hasNot() ? 4 /* NOT + whitespace */ : 0) +
                                    6 /* MEMBER */ +
                                    (expression.hasSpaceAfterMember() ? 1 : 0) +
                                    (expression.hasOf() ? 2 : 0) +
                                    (expression.hasSpaceAfterOf() ? 1 : 0);

                int endPosition = startPosition + length(pathExpression);

                addProblem(
                    expression,
                    startPosition,
                    endPosition,
                    CollectionValuedPathExpression_NotCollectionType,
                    expression.toParsedText()
                );
            }
        }

        super.visit(expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(CollectionValuedPathExpression expression) {
        validatePathExpression(expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(ComparisonExpression expression) {

        // Missing left expression
        if (!expression.hasLeftExpression()) {
            int startPosition = position(expression);
            addProblem(expression, startPosition, ComparisonExpression_MissingLeftExpression);
        }

        // Missing right expression
        if (!expression.hasRightExpression()) {

            int startPosition = position(expression) +
                                (expression.hasLeftExpression() ? 1 : 0) +
                                length(expression.getLeftExpression()) +
                                expression.getComparisonOperator().length() +
                                (expression.hasSpaceAfterIdentifier() ? 1 : 0);

            addProblem(expression, startPosition, ComparisonExpression_MissingRightExpression);
        }

        super.visit(expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(ConcatExpression expression) {

        validateAbstractSingleEncapsulatedExpression(expression, concatExpressionHelper());

        if (expression.hasLeftParenthesis() &&
            expression.hasExpression()) {

            CollectionExpression collectionExpression = getCollectionExpression(expression.getExpression());

            // Single element
            if (collectionExpression == null) {
                addProblem(expression, ConcatExpression_MissingExpression);
            }
            else {
                for (Expression child : collectionExpression.children()) {
                    if (!isValid(child, expression.getEncapsulatedExpressionQueryBNFId())) {
                        addProblem(child, ConcatExpression_InvalidExpression, child.toParsedText());
                    }
                }
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(ConstructorExpression expression) {

        String className = expression.getClassName();

        // Missing constructor name
        if (className.length() == 0) {

            int startPosition = position(expression) +
                                3 /* NEW */ +
                                (expression.hasSpaceAfterNew() ? 1 : 0);

            addProblem(expression, startPosition, ConstructorExpression_MissingConstructorName);
        }
        // Missing '('
        else if (!expression.hasLeftParenthesis()) {

            int startPosition = position(expression) +
                                3 /* NEW */ +
                                (expression.hasSpaceAfterNew() ? 1 : 0) +
                                className.length();

            addProblem(expression, startPosition, ConstructorExpression_MissingLeftParenthesis);
        }
        else {

            // Missing constructor items
            if (!expression.hasConstructorItems()) {

                int startPosition = position(expression) +
                                    3 /* NEW */ +
                                    (expression.hasSpaceAfterNew() ? 1 : 0) +
                                    className.length() +
                                    1 /* '(' */;

                addProblem(expression, startPosition, ConstructorExpression_MissingConstructorItem);
            }
            else {

                // Validate the constructor items
                validateCollectionSeparatedByComma(
                    expression.getConstructorItems(),
                    ConstructorExpression_ConstructorItemEndsWithComma,
                    ConstructorExpression_ConstructorItemIsMissingComma
                );

                // Missing ')'
                if (expression.hasLeftParenthesis()  &&
                    expression.hasConstructorItems() &&
                   !expression.hasRightParenthesis()) {

                    int startPosition = position(expression) +
                                        3 /* NEW */ +
                                        (expression.hasSpaceAfterNew() ? 1 : 0) +
                                        className.length() +
                                        1 /* '(' */ +
                                        length(expression.getConstructorItems());

                    addProblem(expression, startPosition, ConstructorExpression_MissingRightParenthesis);
                }
            }

            super.visit(expression);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(CountFunction expression) {
        validateAggregateFunctionLocation(expression, countFunctionHelper());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(DateTime expression) {

        String dateTime = expression.getText();

        // The JDBC escape syntax
        if (dateTime.startsWith("{")) {
            int length = dateTime.length();

            // Missing opening
            if (!dateTime.startsWith("{d ") &&
                !dateTime.startsWith("{t ") &&
                !dateTime.startsWith("{ts ")) {

                int startPosition = position(expression) + 1;
                int endPosition = startPosition;

                for (int index = 1; index < length; index++) {
                    if (Character.isWhitespace(dateTime.charAt(index))) {
                        break;
                    }
                    endPosition++;
                }

                addProblem(expression, startPosition, endPosition, DateTime_JDBCEscapeFormat_InvalidSpecification);
            }
            // Missing open quote
            else if (!dateTime.startsWith("{d '") &&
                     !dateTime.startsWith("{t '") &&
                     !dateTime.startsWith("{ts '")) {

                int startPosition = position(expression) + 1;

                for (int index = 1; index < length; index++) {
                    startPosition++;

                    if (Character.isWhitespace(dateTime.charAt(index))) {
                        break;
                    }
                }

                addProblem(expression, startPosition, DateTime_JDBCEscapeFormat_MissingOpenQuote);
            }

            // Missing closing '
            if ((length > 1) && (dateTime.charAt(length - (dateTime.endsWith("}") ? 2 : 1)) != '\'')) {
                int startPosition = position(expression) + length;

                if (dateTime.endsWith("}")) {
                    startPosition--;
                }

                addProblem(expression, startPosition, DateTime_JDBCEscapeFormat_MissingCloseQuote);
            }
            // Missing closing }
            else if (!dateTime.endsWith("}")) {
                int startPosition = position(expression) +length;
                addProblem(expression, startPosition, DateTime_JDBCEscapeFormat_MissingRightCurlyBrace);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(DeleteClause expression) {

        // Missing FROM
        if (!expression.hasFrom()) {

            int startPosition = 6 /* DELETE */ +
                                (expression.hasSpaceAfterDelete() ? 1 : 0);

            addProblem(expression, startPosition, DeleteClause_FromMissing);
        }
        // Missing range variable declaration
        else if (!expression.hasRangeVariableDeclaration()) {
            // The whitespace is added to the position regardless if it was parsed or not
            int startPosition = 12 /* DELETE FROM + whitespace) */;
            addProblem(expression, startPosition, DeleteClause_RangeVariableDeclarationMissing);
        }

        // Validate range variable declaration
        if (expression.hasRangeVariableDeclaration()) {

            // More than one entity abstract schema type is declared
            CollectionExpression collectionExpression = getCollectionExpression(expression.getRangeVariableDeclaration());

            if (collectionExpression != null) {
                Expression firstChild = collectionExpression.getChild(0);
                int startPosition = position(firstChild) + length(firstChild);
                int endPosition = position(collectionExpression) + length(collectionExpression);
                boolean malformed = false;

                for (int index = collectionExpression.childrenSize() - 1; --index >= 0; ) {
                    if (!collectionExpression.hasComma(index)) {
                        malformed = true;
                    }
                }

                if (collectionExpression.toActualText().endsWith(" ")) {
                    endPosition--;
                }

                addProblem(
                    expression,
                    startPosition,
                    endPosition,
                    malformed ? DeleteClause_RangeVariableDeclarationMalformed :
                                DeleteClause_MultipleRangeVariableDeclaration
                );
            }
            else {
                super.visit(expression);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(DeleteStatement expression) {
        // Nothing to validate, done directly by DeleteClause and WhereClause
        super.visit(expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(DivisionExpression expression) {
        validateArithmeticExpression(expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(EmptyCollectionComparisonExpression expression) {

        // Missing collection valued path expression
        if (!expression.hasExpression()) {
            int startPosition = position(expression);
            addProblem(expression, startPosition, EmptyCollectionComparisonExpression_MissingExpression);
        }
        else {
            Expression pathExpression = expression.getExpression();

            // The expression is not a path expression
            if (!isValid(pathExpression, CollectionValuedPathExpressionBNF.ID)) {

                int startPosition = position(expression);
                int endPosition   = startPosition + length(pathExpression);

                addProblem(
                    expression,
                    startPosition,
                    endPosition,
                    CollectionValuedPathExpression_NotCollectionType,
                    expression.toParsedText()
                );
            }
            else {
                super.visit(expression);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(EntityTypeLiteral expression) {

        if (isJPA1_0()) {
            addProblem(expression, EntityTypeLiteral_InvalidJPAVersion);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(EntryExpression expression) {

        // JPA 1.0 does not support an ENTRY expression
        if (isJPA1_0()) {
            addProblem(expression, EntryExpression_InvalidJPAVersion);
        }
        else {
            validateAbstractSingleEncapsulatedExpression(expression, entryExpressionHelper());
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(ExistsExpression expression) {
        validateAbstractSingleEncapsulatedExpression(expression, existsExpressionHelper());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(FromClause expression) {
        validateAbstractFromClause(expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(FunctionExpression expression) {

        JPQLQueryBNF queryBNF = getQueryBNF(expression.getQueryBNF().getId());
        boolean validFunction = (queryBNF != null) && queryBNF.hasIdentifier(expression.getIdentifier());

        // JPA 1.0/2.0 does not support the function expression
        if (!isJPA2_1() || !validFunction) {
            addProblem(expression, FunctionExpression_InvalidJPAVersion);
        }
        else {
            validateAbstractSingleEncapsulatedExpression(expression, functionExpressionHelper());

            // Missing function name
            if (expression.hasLeftParenthesis()) {
                String functionName = expression.getUnquotedFunctionName();

                if (ExpressionTools.stringIsEmpty(functionName)) {
                    int startPosition = position(expression) +
                                        expression.getIdentifier().length() +
                                        (expression.hasLeftParenthesis() ? 1 : 0);

                    addProblem(expression, startPosition, FunctionExpression_MissingFunctionName);
                }
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(GroupByClause expression) {

        // Missing grouping items
        if (!expression.hasGroupByItems()) {

            int startPosition = position(expression) +
                                8 /* GROUP BY */ +
                                (expression.hasSpaceAfterGroupBy() ? 1 : 0);

            addProblem(expression, startPosition, GroupByClause_GroupByItemMissing);
        }
        // Validate the separation of multiple ordering items
        else {
            validateCollectionSeparatedByComma(
                expression.getGroupByItems(),
                GroupByClause_GroupByItemEndsWithComma,
                GroupByClause_GroupByItemIsMissingComma
            );

            super.visit(expression);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(HavingClause expression) {
        validateAbstractConditionalClause(
            expression,
            HavingClause_MissingConditionalExpression,
            HavingClause_InvalidConditionalExpression
        );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(IdentificationVariable expression) {

        if (!expression.isVirtual()) {
            String variable = expression.getText();

            validateIdentifier(
                expression,
                variable,
                variable.length(),
                IdentificationVariable_Invalid_ReservedWord,
                IdentificationVariable_Invalid_JavaIdentifier
            );
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(IdentificationVariableDeclaration expression) {
        validateIdentificationVariableDeclaration(expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(IndexExpression expression) {

        // JPA 1.0 does not support an INDEX expression
        if (isJPA1_0()) {
            addProblem(expression, IndexExpression_InvalidJPAVersion);
        }
        else {
            validateAbstractSingleEncapsulatedExpression(expression, indexExpressionHelper());
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(InExpression expression) {

        // Missing expression
        if (!expression.hasExpression()) {
            int startPosition = position(expression);
            addProblem(expression, startPosition, InExpression_MissingExpression);
        }
        else {
            Expression leftExpression = expression.getExpression();
            JPQLQueryBNF queryBNF = getQueryBNF(expression.getExpressionExpressionQueryBNFId());

            // First check for nested array support
            if (isNestedArray(leftExpression)) {
                if (!queryBNF.handlesNestedArray()) {
                    int startPosition = position(expression);
                    int endPosition   = startPosition + length(leftExpression);
                    addProblem(expression, startPosition, endPosition, InExpression_InvalidExpression);
                }
                else {
                    leftExpression.accept(this);
                }
            }
            // Validate the expression
            else if (!isValid(leftExpression, queryBNF) || isDateTimeConstant(leftExpression)) {
                int startPosition = position(expression);
                int endPosition   = startPosition + length(leftExpression);
                addProblem(expression, startPosition, endPosition, InExpression_InvalidExpression);
            }
            else {
                leftExpression.accept(this);
            }
        }

        // Check for "IN :input_parameter" defined in JPA 2.0
        boolean singleInputParameter = isNewerThanOrEqual(JPAVersion.VERSION_2_0) &&
                                       expression.isSingleInputParameter();

        // Missing '('
        if (!expression.hasLeftParenthesis() && !singleInputParameter) {

            int startPosition = position(expression) +
                                length(expression.getExpression()) +
                                (expression.hasExpression() ? 1 : 0) +
                                (expression.hasNot() ? 4 /* NOT + whitespace */ : 0) +
                                2 /* IN */;

            addProblem(expression, startPosition, InExpression_MissingLeftParenthesis);
        }
        // There must be at least one element in the comma separated list that
        // defines the set of values for the IN expression.
        else if (!expression.hasInItems()) {

            int startPosition = position(expression) +
                                length(expression.getExpression()) +
                                (expression.hasExpression() ? 1 : 0) +
                                (expression.hasNot() ? 4 /* NOT + whitespace */ : 0) +
                                2 /* IN */ +
                                (expression.hasSpaceAfterIn() ? 1 : 0) +
                                (expression.hasLeftParenthesis() ? 1 : 0);

            addProblem(expression, startPosition, InExpression_MissingInItems);
        }
        // Make sure the IN items are separated by commas
        else if (!singleInputParameter) {

            Expression inItems = expression.getInItems();
            CollectionExpression collectionExpression = getCollectionExpression(inItems);

            // Validate the collection of items
            if (collectionExpression != null) {

                validateCollectionSeparatedByComma(
                    inItems,
                    InExpression_ItemEndsWithComma,
                    InExpression_ItemIsMissingComma
                );

                // Validate each item
                JPQLQueryBNF queryBNF = getQueryBNF(expression.getExpressionItemQueryBNFId());
                int index = 0;

                for (Expression child : collectionExpression.children()) {

                    index++;

                    // First check for nested array support
                    if (isNestedArray(child)) {
                        if (!queryBNF.handlesNestedArray()) {
                            addProblem(child, InExpression_ItemInvalidExpression, String.valueOf(index));
                        }
                        else {
                            child.accept(this);
                        }
                    }
                    // Invalid item
                    else if (!isValid(child, queryBNF)) {
                        addProblem(child, InExpression_ItemInvalidExpression, String.valueOf(index));
                    }
                    // Validate the item
                    else {
                        child.accept(this);
                    }
                }
            }
            // The single item is invalid
            else if (!isValid(inItems, expression.getExpressionItemQueryBNFId())) {
                addProblem(inItems, InExpression_ItemInvalidExpression);
            }
            // Validate the single item
            else {
                inItems.accept(this);
            }
        }

        // Missing ')'
        if (!singleInputParameter    &&
             expression.hasInItems() &&
            !expression.hasRightParenthesis()) {

            int startPosition = position(expression) +
                                length(expression.getExpression()) +
                                (expression.hasExpression() ? 1 : 0) +
                                (expression.hasNot() ? 4 /* NOT + whitespace */ : 0) +
                                2 /* IN */ +
                                (expression.hasSpaceAfterIn() ? 1 : 0) +
                                (expression.hasLeftParenthesis() ? 1 : 0) +
                                length(expression.getInItems());

            addProblem(expression, startPosition, InExpression_MissingRightParenthesis);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(InputParameter expression) {

        inputParameters.add(expression);
        String parameter = expression.getParameter();

        // No parameter specified
        if (parameter.length() == 1) {
            int startPosition = position(expression);
            addProblem(expression, startPosition, startPosition + 1, InputParameter_MissingParameter);
        }
        // Named parameter: It follows the rules for identifiers defined in Section 4.4.1 of the spec
        else if (expression.isNamed()) {
            if (!isValidJavaIdentifier(parameter.substring(1))) {
                int startPosition = position(expression);
                int endPosition   = startPosition + parameter.length();
                addProblem(expression, startPosition, endPosition, InputParameter_JavaIdentifier);
            }
        }
        // Positional parameter: Designated by the question mark (?) prefix followed by an integer
        else {
            boolean valid = true;

            for (int index = parameter.length(); --index > 0; ) /* Skip ? */ {
                char character = parameter.charAt(index);

                if (!Character.isDigit(character)) {
                    int startPosition = position(expression);
                    int endPosition   = startPosition + parameter.length();
                    addProblem(expression, startPosition, endPosition, InputParameter_NotInteger);
                    valid = false;
                    break;
                }
            }

            // Input parameters are numbered starting from 1
            if (valid) {
                Integer value = Integer.valueOf(parameter.substring(1));

                if (value < 1) {
                    int startPosition = position(expression);
                    int endPosition   = startPosition + parameter.length();
                    addProblem(expression, startPosition, endPosition, InputParameter_SmallerThanOne);
                }
            }
        }

        // Input parameters can only be used in the WHERE or HAVING clause of a query.
        // Skip the ORDER BY clause because it has its own validation rule. The exception
        // to this rule is in a FUNC expression
        validateOwningClause(expression, parameter);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(Join expression) {

        boolean joinFetch = expression.hasFetch();

        // Validate the JOIN identifier
        String identifier = expression.getIdentifier();

        if (identifier != JOIN             &&
            identifier != JOIN_FETCH       &&
            identifier != INNER_JOIN       &&
            identifier != INNER_JOIN_FETCH &&
            identifier != LEFT_JOIN        &&
            identifier != LEFT_JOIN_FETCH  &&
            identifier != LEFT_OUTER_JOIN  &&
            identifier != LEFT_OUTER_JOIN_FETCH) {

            int startPosition = position(expression);
            int endPosition   = startPosition + identifier.length();

            addProblem(
                expression,
                startPosition,
                endPosition,
                Join_InvalidIdentifier
            );
        }

        // Missing join association path expression
        if (!expression.hasJoinAssociationPath()) {

            int startPosition = position(expression) +
                                identifier.length() +
                                (expression.hasSpaceAfterJoin() ? 1 : 0);

            addProblem(
                expression,
                startPosition,
                joinFetch ? JoinFetch_MissingJoinAssociationPath : Join_MissingJoinAssociationPath
            );
        }
        else {

            Expression joinAssociationPath = expression.getJoinAssociationPath();

            // Invalid join association path
            if (!isValid(joinAssociationPath, JoinAssociationPathExpressionBNF.ID)) {

                int startPosition = position(joinAssociationPath);
                int endPosition   = startPosition + length(joinAssociationPath);
                addProblem(expression, startPosition, endPosition, Join_InvalidJoinAssociationPath);
            }
            // Validate join association path
            else {
                joinAssociationPath.accept(this);
            }
        }

        // Missing identification variable
        // A JOIN expression always needs an identification variable
        // A JOIN FETCH expression does not always require an identification, only if 'AS' is present
        if (expression.hasJoinAssociationPath() &&
           !expression.hasIdentificationVariable() &&
            (!joinFetch || expression.hasAs() && isJoinFetchIdentifiable())) {

            int startPosition = position(expression) +
                                identifier.length() +
                                (expression.hasSpaceAfterJoin() ? 1 : 0) +
                                length(expression.getJoinAssociationPath()) +
                                (expression.hasSpaceAfterJoinAssociation() ? 1 : 0) +
                                (expression.hasAs() ? 2 : 0) +
                                (expression.hasSpaceAfterAs() ? 1 : 0);

            addProblem(
                expression,
                startPosition,
                joinFetch ? JoinFetch_MissingIdentificationVariable : Join_MissingIdentificationVariable
            );
        }
        // A JOIN FETCH expression that cannot be identified with an identification variable
        else if (joinFetch &&
                 !isJoinFetchIdentifiable() &&
                 (expression.hasAs() || expression.hasIdentificationVariable())) {

            int startPosition = position(expression) +
                                identifier.length() +
                                (expression.hasSpaceAfterJoin() ? 1 : 0) +
                                length(expression.getJoinAssociationPath()) +
                                (expression.hasSpaceAfterJoinAssociation() ? 1 : 0);

            int endPosition = startPosition +
                              (expression.hasAs() ? 2 : 0) +
                              (expression.hasSpaceAfterAs() ? 1 : 0) +
                              length(expression.getIdentificationVariable());

            addProblem(expression, startPosition, endPosition, JoinFetch_InvalidIdentification);
        }
        else {
            expression.getIdentificationVariable().accept(this);
        }

        // A JOIN FETCH expression can only be defined in the top-level query
        if (joinFetch && isOwnedBySubFromClause(expression)) {
            int startPosition = position(expression);
            int endPosition = startPosition + length(expression);
            addProblem(expression, startPosition, endPosition, JoinFetch_WrongClauseDeclaration);
        }

        // Traverse the ON clause
        expression.getOnClause().accept(this);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(JPQLExpression expression) {

        // Validate the statement
        if (expression.hasQueryStatement()) {

            expression.getQueryStatement().accept(this);

            // Now that the entire tree was visited, we can validate the input parameters, which were
            // automatically cached. Positional and named parameters must not be mixed in a single query
            validateInputParameters(expression);

            // Should never have an unknown ending statement
            if (expression.hasUnknownEndingStatement()) {

                String unknownStatement = expression.getUnknownEndingStatement().toActualText();

                // Make sure the unknown ending statement is not a whitespace, one is kept for content assist
                if (ExpressionTools.stringIsNotEmpty(unknownStatement)) {
                    int startPosition = length(expression.getQueryStatement());
                    int endPosition   = startPosition + length(expression.getUnknownEndingStatement());
                    addProblem(expression, startPosition, endPosition, JPQLExpression_UnknownEnding);
                }
                // Empty query
                else if (!expression.hasQueryStatement()) {
                    addProblem(expression, 0, length(expression), JPQLExpression_InvalidQuery);
                }
            }
        }
        // Invalid query
        else {
            addProblem(expression, 0, length(expression), JPQLExpression_InvalidQuery);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(KeyExpression expression) {

        // JPA 1.0 does not support a KEY expression
        if (isJPA1_0()) {
            addProblem(expression, KeyExpression_InvalidJPAVersion);
        }
        else {
            validateAbstractSingleEncapsulatedExpression(expression, keyExpressionHelper());
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(KeywordExpression expression) {
        // Nothing to validate
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(LengthExpression expression) {
        validateAbstractSingleEncapsulatedExpression(expression, lengthExpressionHelper());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(LikeExpression expression) {

        // Missing string expression
        if (!expression.hasStringExpression()) {
            int startPosition = position(expression);
            addProblem(expression, startPosition, LikeExpression_MissingStringExpression);
        }

        // Missing pattern value
        if (!expression.hasPatternValue()) {

            int startPosition = position(expression) +
                                length(expression.getStringExpression()) +
                                4 /* LIKE */ +
                                (expression.hasSpaceAfterStringExpression() ? 1 : 0) +
                                (expression.hasNot() ? 1 : 0) +
                                (expression.hasSpaceAfterLike() ? 1 : 0);

            addProblem(expression, startPosition, LikeExpression_MissingPatternValue);
        }

        // Validate the escape character
        if (expression.hasEscape()) {

            // Missing escape character
            if (!expression.hasEscapeCharacter()) {

                int startPosition = position(expression) +
                                    length(expression.getStringExpression()) +
                                    4 /* LIKE */ +
                                    (expression.hasSpaceAfterStringExpression() ? 1 : 0) +
                                    (expression.hasNot() ? 1 : 0) +
                                    (expression.hasSpaceAfterLike() ? 1 : 0) +
                                    length(expression.getPatternValue()) +
                                    (expression.hasSpaceAfterPatternValue() ? 1 : 0) +
                                    6 + /* ESCAPE */ +
                                    (expression.hasSpaceAfterEscape() ? 1 : 0);

                addProblem(expression, startPosition, LikeExpression_MissingEscapeCharacter);
            }
            else {
               validateLikeExpressionEscapeCharacter(expression);
           }
        }

        super.visit(expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(LocateExpression expression) {
        validateAbstractTripleEncapsulatedExpression(expression, locateExpressionHelper());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(LowerExpression expression) {
        validateAbstractSingleEncapsulatedExpression(expression, lowerExpressionHelper());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(MaxFunction expression) {
        validateAggregateFunctionLocation(expression, maxFunctionHelper());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(MinFunction expression) {
        validateAggregateFunctionLocation(expression, minFunctionHelper());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(ModExpression expression) {
        validateAbstractDoubleEncapsulatedExpression(expression, modExpressionHelper());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(MultiplicationExpression expression) {
        validateArithmeticExpression(expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(NotExpression expression) {

        // Missing expression
        if (!expression.hasExpression()) {

            int startPosition = position(expression) +
                                3 /* NOT */ +
                                (expression.hasSpaceAfterNot() ? 1 : 0);

            addProblem(expression, startPosition, NotExpression_MissingExpression);
        }
        else {
            super.visit(expression);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(NullComparisonExpression expression) {

        // Missing expression
        if (!expression.hasExpression()) {
            int startPosition = position(expression);
            addProblem(expression, startPosition,  NullComparisonExpression_MissingExpression);
        }
        else {
            super.visit(expression);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(NullExpression expression) {
        // Nothing to validate
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(NullIfExpression expression) {

        // JPA 1.0 does not support a NULLIF expression
        if (isJPA1_0()) {
            addProblem(expression, NullIfExpression_InvalidJPAVersion);
        }
        else {
            validateAbstractDoubleEncapsulatedExpression(expression, nullIfExpressionHelper());
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(NumericLiteral expression) {

        String text = expression.getText();

        // - Exact numeric literals support the use of Java integer literal syntax as well as SQL
        //   exact numeric literal syntax
        // - Approximate literals support the use Java floating point literal syntax as well as SQL
        //   approximate numeric literal syntax
        // - Appropriate suffixes can be used to indicate the specific type of a numeric literal in
        //   accordance with the Java Language Specification
        if (!isNumericLiteral(text)) {
            int startPosition = position(expression);
            int endPosition   = startPosition + text.length();
            addProblem(expression, startPosition, endPosition, NumericLiteral_Invalid, text);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(ObjectExpression expression) {
        validateAbstractSingleEncapsulatedExpression(expression, objectExpressionHelper());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(OnClause expression) {

        // Missing conditional expression
        if (!expression.hasConditionalExpression()) {

            int startPosition = position(expression) +
                                2 /* ON */ +
                                (expression.hasSpaceAfterIdentifier() ? 1 : 0);

            addProblem(expression, startPosition, OnClause_MissingConditionalExpression);
        }
        else {
            Expression conditionalExpression = expression.getConditionalExpression();

            // Invalid conditional expression
            if (!isValid(conditionalExpression, ConditionalExpressionBNF.ID)) {

                int startPosition = position(expression) +
                                    2 /* ON */ +
                                    (expression.hasSpaceAfterIdentifier() ? 1 : 0);

                int endPosition = startPosition + length(conditionalExpression);

                addProblem(expression, startPosition, endPosition, OnClause_InvalidConditionalExpression);
            }
            // Validate the conditional expression
            else {
                conditionalExpression.accept(this);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(OrderByClause expression) {

        if (!expression.hasOrderByItems()) {
            int startPosition = position(expression.getOrderByItems());
            addProblem(expression, startPosition, OrderByClause_OrderByItemMissing);
        }
        // Validate the separation of multiple grouping items
        else {
            validateCollectionSeparatedByComma(
                expression.getOrderByItems(),
                OrderByClause_OrderByItemEndsWithComma,
                OrderByClause_OrderByItemIsMissingComma
            );
        }

        super.visit(expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(OrderByItem expression) {

        // Missing ordering item
        if (!expression.hasExpression()) {
            int startPosition = position(expression);
            addProblem(expression, startPosition, OrderByItem_MissingExpression);
        }
        else {
            Expression item = expression.getExpression();

            // Invalid order by item
            if (!isValid(item, InternalOrderByItemBNF.ID)) {
                int startPosition = position(item);
                int endPosition   = startPosition + length(item);
                addProblem(item, startPosition, endPosition, OrderByItem_InvalidExpression);
            }
            else {
                super.visit(expression);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(OrExpression expression) {
        validateLogicalExpression(
            expression,
            ConditionalExpressionBNF.ID,
            ConditionalExpressionBNF.ID
        );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(RangeVariableDeclaration expression) {

        // Missing abstract schema name
        if (!expression.hasRootObject()) {
            int startPosition = position(expression);
            addProblem(expression, startPosition, RangeVariableDeclaration_MissingRootObject);
        }
        else {

            Expression rootObject = expression.getRootObject();

            if (!isValid(rootObject, RangeDeclarationBNF.ID)) {

                int startPosition = position(rootObject);
                int endPosition   = startPosition + length(rootObject);

                addProblem(expression, startPosition, endPosition, RangeVariableDeclaration_InvalidRootObject);
            }
            else {
                rootObject.accept(this);
            }
        }

        // Missing identification variable
        if (!expression.hasIdentificationVariable() &&
            !expression.hasVirtualIdentificationVariable()) {

            int startPosition = position(expression) +
                                length(expression.getRootObject()) +
                                (expression.hasSpaceAfterRootObject() ? 1 : 0) +
                                (expression.hasAs() ? 2 : 0) +
                                (expression.hasSpaceAfterAs() ? 1 : 0);

            addProblem(expression, startPosition, RangeVariableDeclaration_MissingIdentificationVariable);
        }
        else {
            expression.getIdentificationVariable().accept(this);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(ResultVariable expression) {

        // JPA 1.0 does not support a result variable expression
        if (isJPA1_0()) {
            int startPosition = position(expression) +
                                length(expression.getSelectExpression()) +
                                (expression.hasSelectExpression() ? 1 : 0);

            int endPosition = startPosition +
                              (expression.hasAs() ? 2 : 0) +
                              (expression.hasSpaceAfterAs() ? 1 : 0) +
                              length(expression.getResultVariable());

            addProblem(expression, startPosition, endPosition, ResultVariable_InvalidJPAVersion);
        }
        else {

            // Missing select expression
            if (!expression.hasSelectExpression()) {
                int startPosition = position(expression);
                addProblem(expression, startPosition, ResultVariable_MissingSelectExpression);
            }
            // Validate the select expression
            else {
                expression.getSelectExpression().accept(this);
            }

            // Missing result variable
            if (!expression.hasResultVariable()) {

                int startPosition = position(expression) +
                                    length(expression.getSelectExpression()) +
                                    (expression.hasSelectExpression() ? 1 : 0) +
                                    (expression.hasAs() ? 2 : 0) +
                                    (expression.hasSpaceAfterAs() ? 1 : 0);

                addProblem(expression, startPosition, ResultVariable_MissingResultVariable);
            }
            // Validate the result variable
            else {
                expression.getResultVariable().accept(this);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(SelectClause expression) {

        validateAbstractSelectClause(expression, true);

        // Make sure the select expression are separated by a comma
        if (expression.hasSelectExpression()) {
            validateCollectionSeparatedByComma(
                expression.getSelectExpression(),
                AbstractSelectClause_SelectExpressionEndsWithComma,
                AbstractSelectClause_SelectExpressionIsMissingComma
            );
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(SelectStatement expression) {
        validateAbstractSelectStatement(expression);
        super.visit(expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(SimpleFromClause expression) {
        validateAbstractFromClause(expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(SimpleSelectClause expression) {
        validateAbstractSelectClause(expression, isMultipleSubquerySelectItemsAllowed(expression));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(SimpleSelectStatement expression) {
        validateSimpleSelectStatement(expression);
        super.visit(expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(SizeExpression expression) {
        validateAbstractSingleEncapsulatedExpression(expression, sizeExpressionHelper());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(SqrtExpression expression) {
        validateAbstractSingleEncapsulatedExpression(expression, sqrtExpressionHelper());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(StateFieldPathExpression expression) {
        validatePathExpression(expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(StringLiteral expression) {
        if (!expression.hasCloseQuote()) {
            addProblem(expression, StringLiteral_MissingClosingQuote);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(SubExpression expression) {

        // Missing sub-expression
        if (!expression.hasExpression()) {
            int startPosition = position(expression) + 1;
            addProblem(expression, startPosition, SubExpression_MissingExpression);
        }
        else {

            // Missing right parenthesis
            if (!expression.hasRightParenthesis()) {

                int startPosition = position(expression) +
                                    1 /* ( */ +
                                    length(expression.getExpression());

                addProblem(expression, startPosition, SubExpression_MissingRightParenthesis);
            }

            super.visit(expression);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(SubstringExpression expression) {
        validateAbstractTripleEncapsulatedExpression(expression, substringExpressionHelper());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(SubtractionExpression expression) {
        validateArithmeticExpression(expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(SumFunction expression) {
        validateAggregateFunctionLocation(expression, sumFunctionHelper());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(TreatExpression expression) {

        // EclipseLink 1.0 does not support TREAT expression
        if (isJPA1_0()) {
            addProblem(expression, TreatExpression_InvalidJPAPlatform);
        }
        else  {
            // TODO
            super.visit(expression);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(TrimExpression expression) {

        validateAbstractSingleEncapsulatedExpression(expression, trimExpressionHelper());

        // Missing string primary
        if (!expression.hasExpression()) {

            int startPosition = position(expression) +
                                4 /* TRIM */ +
                                (expression.hasLeftParenthesis() ? 1 : 0) +
                                expression.getSpecification().getValue().length() +
                                (expression.hasSpaceAfterSpecification() ? 1 : 0) +
                                length(expression.getTrimCharacter()) +
                                (expression.hasSpaceAfterTrimCharacter() ? 1 : 0) +
                                (expression.hasFrom() ? 4 : 0) +
                                (expression.hasSpaceAfterFrom() ? 1 : 0);

            addProblem(expression, startPosition, TrimExpression_MissingExpression);
        }
        // Invalid string primary
        else if (!isValid(expression.getExpression(), expression.getEncapsulatedExpressionQueryBNFId())) {

            int startPosition = position(expression) +
                                4 /* TRIM */ +
                                (expression.hasLeftParenthesis() ? 1 : 0) +
                                expression.getSpecification().getValue().length() +
                                (expression.hasSpaceAfterSpecification() ? 1 : 0) +
                                length(expression.getTrimCharacter()) +
                                (expression.hasSpaceAfterTrimCharacter() ? 1 : 0) +
                                (expression.hasFrom() ? 4 : 0) +
                                (expression.hasSpaceAfterFrom() ? 1 : 0);

            int endPosition = startPosition + length(expression.getExpression());
            addProblem(expression, startPosition, endPosition, TrimExpression_InvalidExpression);
        }

        // Invalid trim character
        if (expression.hasTrimCharacter()) {

            Expression trimCharacter = expression.getTrimCharacter();

            // Make sure it's not an input parameter
            String inputParameter = literal(trimCharacter, LiteralType.INPUT_PARAMETER);

            if (ExpressionTools.stringIsEmpty(inputParameter)) {

                String stringLiteral = literal(trimCharacter, LiteralType.STRING_LITERAL);

                int startPosition = position(expression) +
                                    4 /* TRIM */ +
                                    (expression.hasLeftParenthesis() ? 1 : 0) +
                                    expression.getSpecification().getValue().length() +
                                    (expression.hasSpaceAfterSpecification() ? 1 : 0);

                int endPosition = startPosition + length(trimCharacter);

                if (ExpressionTools.stringIsEmpty(stringLiteral)) {
                    addProblem(trimCharacter, startPosition, endPosition, TrimExpression_InvalidTrimCharacter);
                }
                else {
                    stringLiteral = stringLiteral.substring(1, stringLiteral.length() - (stringLiteral.endsWith("'") ? 1 : 0));

                    if (stringLiteral.length() != 1) {
                        addProblem(trimCharacter, startPosition, endPosition, TrimExpression_NotSingleStringLiteral);
                    }
                }
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(TypeExpression expression) {

        // JPA 1.0 does not support a TYPE expression
        if (isJPA1_0()) {
            addProblem(expression, TypeExpression_InvalidJPAVersion);
        }
        else {
            validateAbstractSingleEncapsulatedExpression(expression, typeExpressionHelper());
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(UnknownExpression expression) {
        // Nothing to validate and we don't want to validate its encapsulated expression
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(UpdateClause expression) {

        // Missing range variable declaration
        if (!expression.hasRangeVariableDeclaration()) {

            int startPosition = position(expression) +
                                6 /* UPDATE */ +
                                (expression.hasSpaceAfterUpdate() ? 1 : 0);

            addProblem(expression, startPosition, UpdateClause_MissingRangeVariableDeclaration);
        }
        // Missing 'SET'
        else if (!expression.hasSet()) {

            int startPosition = position(expression) +
                                6 /* UPDATE */ +
                                (expression.hasSpaceAfterUpdate() ? 1 : 0) +
                                length(expression.getRangeVariableDeclaration()) +
                                (expression.hasSpaceAfterRangeVariableDeclaration() ? 1 : 0);

            addProblem(expression, startPosition, UpdateClause_MissingSet);
        }
        // Missing update items
        else if (!expression.hasUpdateItems()) {

            int startPosition = position(expression) +
                                6 /* UPDATE */ +
                                (expression.hasSpaceAfterUpdate() ? 1 : 0) +
                                length(expression.getRangeVariableDeclaration()) +
                                (expression.hasSpaceAfterRangeVariableDeclaration() ? 1 : 0) +
                                3 /* 'SET' */ +
                                (expression.hasSpaceAfterSet() ? 1 : 0);

            addProblem(expression, startPosition, UpdateClause_MissingUpdateItems);
        }
        // Make sure the update items are separated by commas
        else {
            validateCollectionSeparatedByComma(
                expression.getUpdateItems(),
                UpdateClause_UpdateItemEndsWithComma,
                UpdateClause_UpdateItemIsMissingComma
            );
        }

        super.visit(expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(UpdateItem expression) {

        // Missing state field path expression
        if (!expression.hasStateFieldPathExpression()) {
            int startPosition = position(expression);
            addProblem(expression, startPosition, UpdateItem_MissingStateFieldPathExpression);
        }

        // Missing '='
        if (expression.hasStateFieldPathExpression() &&
           !expression.hasEqualSign()) {

            int startPosition = position(expression) +
                                length(expression.getStateFieldPathExpression()) +
                                (expression.hasSpaceAfterStateFieldPathExpression() ? 1 : 0);

            addProblem(expression, startPosition, UpdateItem_MissingEqualSign);
        }
        // After '='
        else if (expression.hasEqualSign()) {

            // Missing new value
            if (!expression.hasNewValue()) {

                int startPosition = position(expression) +
                                    length(expression.getStateFieldPathExpression()) +
                                    (expression.hasSpaceAfterStateFieldPathExpression() ? 1 : 0) +
                                    1 /* '=' */ +
                                    (expression.hasSpaceAfterEqualSign() ? 1 : 0);

                addProblem(expression, startPosition, UpdateItem_MissingNewValue);
            }
            // Invalid new value
            else {
                // TODO: Anything to validate?
            }
        }

        super.visit(expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(UpdateStatement expression) {
        // Done directly by UpdateClause and WhereClause
        super.visit(expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(UpperExpression expression) {
        validateAbstractSingleEncapsulatedExpression(expression, upperExpressionHelper());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(ValueExpression expression) {

        // JPA 1.0 does not support a VALUE expression
        if (isJPA1_0()) {
            addProblem(expression, ValueExpression_InvalidJPAVersion);
        }
        else {
            validateAbstractSingleEncapsulatedExpression(expression, valueExpressionHelper());
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(WhenClause expression) {

        // WHEN expression is missing
        if (!expression.hasWhenExpression()) {

            int startPosition = position(expression) +
                                4 /* WHEN */ +
                                (expression.hasSpaceAfterWhen() ? 1 : 0);

            addProblem(expression, startPosition, WhenClause_MissingWhenExpression);
        }

        // THEN identifier is missing
        if (expression.hasWhenExpression() &&
           !expression.hasThen()) {

            int startPosition = position(expression) +
                                4 /* WHEN */ +
                                (expression.hasSpaceAfterWhen() ? 1 : 0) +
                                length(expression.getWhenExpression()) +
                                (expression.hasSpaceAfterWhenExpression() ? 1 : 0);

            addProblem(expression, startPosition, WhenClause_MissingThenIdentifier);
        }

        // THEN expression is missing
        if (expression.hasThen() &&
           !expression.hasThenExpression()) {

            int startPosition = position(expression) +
                                4 /* WHEN */ +
                                (expression.hasSpaceAfterWhen() ? 1 : 0) +
                                length(expression.getWhenExpression()) +
                                (expression.hasSpaceAfterWhenExpression() ? 1 : 0) +
                                4 /* THEN */ +
                                (expression.hasSpaceAfterThen() ? 1 : 0);

            addProblem(expression, startPosition, WhenClause_MissingThenExpression);
        }

        super.visit(expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(WhereClause expression) {
        validateAbstractConditionalClause(
            expression,
            WhereClause_MissingConditionalExpression,
            WhereClause_InvalidConditionalExpression
        );
    }

    // Made static for performance reasons.
    private abstract static class AbstractValidator {

        protected final AbstractGrammarValidator validator;

        protected AbstractValidator(AbstractGrammarValidator validator) {
            this.validator = validator;
        }
    }

    // Made static for performance reasons.
    /**
     * This validate is responsible to validate the collection of {@link Expression Expressions}:
     * <ul>
     * <li>Making sure they are all separated by a comma or by a space (depending on which one is
     * required);</li>
     * <li>Making sure it does not end with a comma;</li>
     * <li>There is no empty expression between two commas.</li>
     * </ul>
     */
    protected abstract static class AbstractCollectionValidator extends AbstractExpressionVisitor {

        String endsWithCommaProblemKey;
        boolean validateOnly;
        String wrongSeparatorProblemKey;
        private final AbstractGrammarValidator validator;

        protected AbstractCollectionValidator(AbstractGrammarValidator validator) {
            this.validator = validator;
        }

        protected void validateEndsWithComma(CollectionExpression expression) {

            if (expression.endsWithComma()) {
                int lastIndex = expression.childrenSize() - 1;
                int length = expression.toParsedText(lastIndex).length();
                int startPosition = validator.position(expression) + length - 1;

                if (expression.endsWithSpace()) {
                    startPosition--;
                }

                int endPosition = startPosition + 1;

                if (!validateOnly) {
                    validator.addProblem(expression, startPosition, endPosition, endsWithCommaProblemKey);
                }
            }
        }

        protected void validateSeparation(CollectionExpression expression) {

            for (int index = 0, count = expression.childrenSize(); index + 1 < count; index++) {

                Expression expression1 = expression.getChild(index);

                if (validator.length(expression1) == 0) {
                    int startPosition = validator.position(expression1);
                    int endPosition   = startPosition;

                    validator.addProblem(
                        expression,
                        startPosition,
                        endPosition,
                        CollectionExpression_MissingExpression,
                        String.valueOf(index + 1)
                    );
                }

                if (!validateSeparator(expression, index)) {
                    Expression expression2 = expression.getChild(index + 1);

                    int startPosition = validator.position(expression1) + validator.length(expression1);
                    int endPosition   = validator.position(expression2);

                    // The space is part of the child expression, move backward
                    if (!expression.hasSpace(index)) {
                        startPosition--;
                    }

                    if (!validateOnly) {
                        validator.addProblem(
                            expression,
                            startPosition,
                            endPosition,
                            wrongSeparatorProblemKey,
                            expression1.toParsedText(),
                            expression2.toParsedText()
                        );
                    }
                }
            }
        }

        /**
         * Validates
         *
         * @param expression
         * @param index
         * @return
         */
        abstract boolean validateSeparator(CollectionExpression expression, int index);

        /**
         * {@inheritDoc}
         */
        @Override
        public void visit(CollectionExpression expression) {
            validateSeparation(expression);
            validateEndsWithComma(expression);
        }
    }

    // Made static for performance reasons.
    protected static abstract class AbstractDoubleEncapsulatedExpressionHelper<T extends AbstractDoubleEncapsulatedExpression>
            extends AbstractValidator implements AbstractEncapsulatedExpressionHelper<T> {

        protected AbstractDoubleEncapsulatedExpressionHelper(AbstractGrammarValidator validator) {
            super(validator);
        }

        /**
         * {@inheritDoc}
         */
        public String[] arguments(T expression) {
            return ExpressionTools.EMPTY_STRING_ARRAY;
        }

        protected abstract String firstExpressionInvalidKey();

        protected int firstExpressionLength(T expression) {
            return validator.length(expression.getFirstExpression());
        }

        protected abstract String firstExpressionMissingKey();

        protected boolean hasComma(T expression) {
            return !hasFirstExpression(expression) ||
                   expression.hasComma();
        }

        protected boolean hasFirstExpression(T expression) {
            return expression.hasFirstExpression();
        }

        /**
         * {@inheritDoc}
         */
        public boolean hasLeftParenthesis(T expression) {
            return expression.hasLeftParenthesis();
        }

        /**
         * {@inheritDoc}
         */
        public boolean hasRightParenthesis(T expression) {
            // If the second encapsulated expression is missing, then no need
            // to add a problem for a missing ')' because the second encapsulated
            // expression needs to be specified first
            return !expression.hasSecondExpression() ||
                    expression.hasRightParenthesis();
        }

        protected boolean hasSecondExpression(T expression) {
            return !expression.hasComma() ||
                    expression.hasSecondExpression();
        }

        /**
         * {@inheritDoc}
         */
        public String identifier(T expression) {
            return expression.getIdentifier();
        }

        protected final boolean isFirstExpressionValid(T expression) {
            return validator.isValid(expression.getFirstExpression(), expression.parameterExpressionBNF(0));
        }

        protected final boolean isSecondExpressionValid(T expression) {
            return validator.isValid(expression.getSecondExpression(), expression.parameterExpressionBNF(1));
        }

        protected abstract String missingCommaKey();

        protected abstract String secondExpressionInvalidKey();

        protected int secondExpressionLength(T expression) {
            return validator.length(expression.getSecondExpression());
        }

        protected abstract String secondExpressionMissingKey();
    }

    // Made static for performance reasons.
    /**
     * The root helper that validates any {@link AbstractEncapsulatedExpression}.
     *
     * @see AbstractDoubleEncapsulatedExpressionHelper
     * @see AbstractSingleEncapsulatedExpressionHelper
     * @see AbstractTripleEncapsulatedExpressionHelper
     */
    protected static interface AbstractEncapsulatedExpressionHelper<T extends AbstractEncapsulatedExpression> {

        /**
         * Returns the arguments that can help to format the localized problem.
         *
         * @param expression The {@link AbstractEncapsulatedExpression} being validated
         * @return The list of arguments used to complete the localized problem
         */
        String[] arguments(T expression);

        /**
         * Determines whether the given {@link AbstractEncapsulatedExpression} has the left parenthesis.
         *
         * @param expression The {@link AbstractEncapsulatedExpression} being validated
         * @return <code>true</code> if the left parenthesis was parsed
         */
        boolean hasLeftParenthesis(T expression);

        /**
         * Determines whether the given {@link AbstractEncapsulatedExpression} has the right parenthesis.
         *
         * @param expression The {@link AbstractEncapsulatedExpression} being validated
         * @return <code>true</code> if the right parenthesis was parsed
         */
        boolean hasRightParenthesis(T expression);

        /**
         * Returns the JPQL identifier of the given {@link AbstractEncapsulatedExpression}.
         *
         * @param expression The {@link AbstractEncapsulatedExpression} being validated
         * @return The JPQL identifier of the given {@link AbstractEncapsulatedExpression}
         */
        String identifier(T expression);

        /**
         * Returns the message key for the problem describing that the left parenthesis is missing.
         *
         * @param expression The {@link AbstractEncapsulatedExpression} being validated
         * @return The key used to retrieve the localized message
         */
        String leftParenthesisMissingKey(T expression);

        /**
         * Returns the message key for the problem describing that the right parenthesis is missing.
         *
         * @param expression The {@link AbstractEncapsulatedExpression} being validated
         * @return The key used to retrieve the localized message
         */
        String rightParenthesisMissingKey(T expression);
    }

    // Made static for performance reasons.
    /**
     * The abstract implementation of {@link AbstractSingleEncapsulatedExpressionHelper} which
     * implements some of the methods since the behavior is the same for all subclasses of
     * {@link AbstractSingleEncapsulatedExpression}.
     */
    protected static abstract class AbstractSingleEncapsulatedExpressionHelper<T extends AbstractSingleEncapsulatedExpression>
            extends AbstractValidator implements AbstractEncapsulatedExpressionHelper<T> {

        protected AbstractSingleEncapsulatedExpressionHelper(AbstractGrammarValidator validator) {
            super(validator);
        }

        /**
         * {@inheritDoc}
         */
        public String[] arguments(T expression) {
            return ExpressionTools.EMPTY_STRING_ARRAY;
        }

        /**
         * Returns the message key for the problem describing that the encapsulated expression is invalid.
         *
         * @param expression The {@link AbstractSingleEncapsulatedExpression} being validated
         * @return The key used to retrieve the localized message
         */
        protected abstract String encapsulatedExpressionInvalidKey(T expression);

        /**
         * Returns the length of the encapsulated expression.
         *
         * @param expression {@link AbstractSingleEncapsulatedExpression} being validated
         * @return The length of the encapsulated expression
         */
        protected int encapsulatedExpressionLength(T expression) {
            return validator.length(expression.getExpression());
        }

        /**
         * Returns the message key for the problem describing that the encapsulated expression is missing.
         *
         * @param expression The {@link AbstractSingleEncapsulatedExpression} being validated
         * @return The key used to retrieve the localized message
         */
        protected abstract String encapsulatedExpressionMissingKey(T expression);

        /**
         * {@inheritDoc}
         */
        public boolean hasLeftParenthesis(T expression) {
            return expression.hasLeftParenthesis();
        }

        /**
         * {@inheritDoc}
         */
        public boolean hasRightParenthesis(T expression) {
            // If the encapsulated expression is missing, then no need to
            // add a problem for a missing ')' because the encapsulated
            // expression needs to be specified first
            return !expression.hasEncapsulatedExpression() ||
                    expression.hasRightParenthesis();
        }

        /**
         * {@inheritDoc}
         */
        public final String identifier(T expression) {
            return expression.getIdentifier();
        }

        /**
         * Determines whether there is an encapsulated expression or not.
         *
         * @param expression The {@link AbstractSingleEncapsulatedExpression} being validated
         * @return <code>true</code> if the given {@link AbstractSingleEncapsulatedExpression} has an
         * encapsulated expression; <code>false</code> otherwise
         */
        protected boolean isEncapsulatedExpressionMissing(T expression) {
            return !expression.hasExpression();
        }

        /**
         * Determines whether the encapsulated expression is valid.
         *
         * @param expression The {@link AbstractSingleEncapsulatedExpression} being validated
         * @return <code>true</code> if the encapsulated expression is valid; <code>false</code>
         * otherwise
         */
        protected boolean isEncapsulatedExpressionValid(T expression) {
            return validator.isValid(expression.getExpression(), expression.getEncapsulatedExpressionQueryBNFId());
        }

        /**
         * Returns the length after the left parenthesis and before the encapsulated expression starts.
         * <p>
         * By default, there is no text after the left parenthesis and the encapsulated expression
         * but there are exceptions, such as the functions (AVG, COUNT, MIN, MAX, SUM).
         *
         * @param expression The {@link AbstractSingleEncapsulatedExpression} being validated
         * @return The length after the left parenthesis and before the encapsulated expression starts
         */
        protected int lengthBeforeEncapsulatedExpression(T expression) {
            return 0;
        }
    }

    // Made static for performance reasons.
    protected abstract static class AbstractTripleEncapsulatedExpressionHelper<T extends AbstractTripleEncapsulatedExpression>
            extends AbstractValidator implements AbstractEncapsulatedExpressionHelper<T> {

        protected AbstractTripleEncapsulatedExpressionHelper(AbstractGrammarValidator validator) {
            super(validator);
        }

        /**
         * {@inheritDoc}
         */
        public String[] arguments(T expression) {
            return ExpressionTools.EMPTY_STRING_ARRAY;
        }

        protected abstract String firstCommaMissingKey();

        protected abstract String firstExpressionInvalidKey();

        protected int firstExpressionLength(T expression) {
            return validator.length(expression.getFirstExpression());
        }

        protected abstract String firstExpressionMissingKey();

        protected boolean hasFirstExpression(T expression) {
            return expression.hasFirstExpression();
        }

        /**
         * {@inheritDoc}
         */
        public boolean hasLeftParenthesis(T expression) {
            return expression.hasLeftParenthesis();
        }

        /**
         * {@inheritDoc}
         */
        public boolean hasRightParenthesis(T expression) {
            return !isRightParenthesisMissing(expression);
        }

        protected boolean hasSecondExpression(T expression) {
            return expression.hasSecondExpression();
        }

        protected boolean hasThirdExpression(T expression) {
            return expression.hasThirdExpression();
        }

        protected boolean isFirstExpressionValid(T expression) {
            return validator.isValid(expression.getFirstExpression(), expression.getParameterQueryBNFId(0));
        }

        /**
         * Determines whether the right parenthesis is missing from the given expression.
         *
         * @param expression The {@link Expression} to verify for the existence of the right parenthesis
         * by determining if the encapsulated information has been parsed or not
         * @return <code>true</code> if the encapsulated information was parsed and the right parenthesis
         * is missing; <code>false</code> in any other case
         */
        protected boolean isRightParenthesisMissing(T expression) {

            if (!expression.hasLeftParenthesis() ||
                !expression.hasFirstExpression() ||
                 expression.hasRightParenthesis()) {

                return false;
            }

            if (expression.hasFirstExpression()  &&
               !expression.hasFirstComma()       &&
               !expression.hasSecondExpression() &&
               !expression.hasSecondComma()      &&
               !expression.hasThirdExpression()) {

                return false;
            }

            if (expression.hasFirstComma()       &&
               !expression.hasSecondExpression() &&
               !expression.hasSecondComma()      &&
               !expression.hasThirdExpression()) {

                return false;
            }

            if (expression.hasSecondExpression() &&
                expression.hasSecondComma()      &&
               !expression.hasThirdExpression()) {

                return false;
            }

            return true;
        }

        protected boolean isSecondExpressionValid(T expression) {
            return validator.isValid(expression.getSecondExpression(), expression.getParameterQueryBNFId(1));
        }

        protected boolean isThirdExpressionValid(T expression) {
            return validator.isValid(expression.getThirdExpression(), expression.getParameterQueryBNFId(2));
        }

        protected abstract String secondCommaMissingKey();

        protected abstract String secondExpressionInvalidKey();

        protected int secondExpressionLength(T expression) {
            return validator.length(expression.getSecondExpression());
        }

        protected abstract String secondExpressionMissingKey();

        protected abstract String thirdExpressionInvalidKey();

        protected int thirdExpressionLength(T expression) {
            return validator.length(expression.getThirdExpression());
        }

        protected abstract String thirdExpressionMissingKey();
    }

    // Made static final for performance reasons.
    /**
     * This visitor retrieves the {@link CollectionExpression} if it is visited.
     */
    protected static final class CollectionExpressionVisitor extends AbstractExpressionVisitor {

        /**
         * The {@link CollectionExpression} if it is the {@link Expression} that was visited.
         */
        protected CollectionExpression expression;

        /**
         * Creates a new <code>CollectionExpressionVisitor</code>.
         */
        protected CollectionExpressionVisitor() {
            super();
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void visit(CollectionExpression expression) {
            this.expression = expression;
        }
    }

    // Made static final for performance reasons.
    /**
     * This validator validates a {@link CollectionExpression} by making sure each item is separated
     * by a comma.
     */
    protected static final class CollectionSeparatedByCommaValidator extends AbstractCollectionValidator {

        protected CollectionSeparatedByCommaValidator(AbstractGrammarValidator validator) {
            super(validator);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        boolean validateSeparator(CollectionExpression expression, int index) {
            return expression.hasComma(index);
        }
    }

    // Made static final for performance reasons.
    /**
     * This validator validates a {@link CollectionExpression} by making sure each item is not
     * separated by a comma.
     */
    protected static final class CollectionSeparatedBySpaceValidator extends AbstractCollectionValidator {

        protected CollectionSeparatedBySpaceValidator(AbstractGrammarValidator validator) {
            super(validator);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        boolean validateSeparator(CollectionExpression expression, int index) {
            return !expression.hasComma(index);
        }
    }

    // Made static final for performance reasons.
    protected static final class ComparisonExpressionVisitor extends AbstractExpressionVisitor {

        /**
         * The {@link ComparisonExpression} if it is the {@link Expression} that was visited.
         */
        ComparisonExpression expression;

        /**
         * {@inheritDoc}
         */
        @Override
        public void visit(ComparisonExpression expression) {
            this.expression = expression;
        }
    }

    // Made static final for performance reasons.
    protected static final class DateTimeVisitor extends AbstractExpressionVisitor {

        /**
         * Determines whether the visited {@link Expression} is {@link DateTime} or not.
         */
        public boolean dateTime;

        /**
         * {@inheritDoc}
         */
        @Override
        public void visit(DateTime expression) {
            dateTime = true;
        }
    }

    // Made static final for performance reasons.
    /**
     * This visitor checks to see if the visited expression is {@link NullExpression}.
     */
    protected static final class NullExpressionVisitor extends AbstractExpressionVisitor {

        /**
         * The {@link NullExpression} if it is the {@link Expression} that was visited.
         */
        protected NullExpression expression;

        /**
         * Creates a new <code>NullExpressionVisitor</code>.
         */
        protected NullExpressionVisitor() {
            super();
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void visit(NullExpression expression) {
            this.expression = expression;
        }
    }
}
