/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.openjpa.kernel.jpql;

import java.io.PrintStream;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.security.AccessController;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.ChronoField;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import java.util.TreeSet;

import org.apache.openjpa.conf.Compatibility;
import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.kernel.BrokerFactory;
import org.apache.openjpa.kernel.ExpressionStoreQuery;
import org.apache.openjpa.kernel.FillStrategy;
import org.apache.openjpa.kernel.QueryContext;
import org.apache.openjpa.kernel.QueryOperations;
import org.apache.openjpa.kernel.ResultShape;
import org.apache.openjpa.kernel.StoreContext;
import org.apache.openjpa.kernel.exps.AbstractExpressionBuilder;
import org.apache.openjpa.kernel.exps.Context;
import org.apache.openjpa.kernel.exps.DateTimeExtractField;
import org.apache.openjpa.kernel.exps.DateTimeExtractPart;
import org.apache.openjpa.kernel.exps.Expression;
import org.apache.openjpa.kernel.exps.ExpressionFactory;
import org.apache.openjpa.kernel.exps.Literal;
import org.apache.openjpa.kernel.exps.Parameter;
import org.apache.openjpa.kernel.exps.Path;
import org.apache.openjpa.kernel.exps.QueryExpressions;
import org.apache.openjpa.kernel.exps.Resolver;
import org.apache.openjpa.kernel.exps.Subquery;
import org.apache.openjpa.kernel.exps.Value;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.util.J2DoPrivHelper;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.Localizer.Message;
import org.apache.openjpa.lib.util.OrderedMap;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.meta.MetaDataRepository;
import org.apache.openjpa.meta.ValueMetaData;
import org.apache.openjpa.util.InternalException;
import org.apache.openjpa.util.UserException;


/**
 * Builder for JPQL expressions. This class takes the query parsed
 * in {@link JPQL} and converts it to an expression tree using
 * an {@link ExpressionFactory}. Public for unit testing purposes.
 *
 * @author Marc Prud'hommeaux
 * @author Patrick Linskey
 */
public class JPQLExpressionBuilder
    extends AbstractExpressionBuilder
    implements JPQLTreeConstants {

    private static final int VAR_PATH = 1;
    private static final int VAR_ERROR = 2;

    private static final Localizer _loc = Localizer.forPackage
        (JPQLExpressionBuilder.class);

    private final Stack<Context> contexts = new Stack<>();
    private OrderedMap<Object, Class<?>> parameterTypes;
    private int aliasCount = 0;
    private boolean inAssignSubselectProjection = false;
    private boolean hasParameterizedInExpression = false;

    /**
     * Constructor.
     *
     * @param factory the expression factory to use
     * @param query used to resolve variables, parameters,
     * and class names used in the query
     * @param parsedQuery the parsed query
     */
    public JPQLExpressionBuilder(ExpressionFactory factory,
        ExpressionStoreQuery query, Object parsedQuery) {
        super(factory, query.getResolver());

        contexts.push(new Context(parsedQuery instanceof ParsedJPQL
            ? (ParsedJPQL) parsedQuery
            : parsedQuery instanceof String
            ? getParsedQuery((String) parsedQuery)
            : null, null, null));

        if (ctx().parsed == null)
            throw new InternalException(parsedQuery + "");
    }

    @Override
    protected Localizer getLocalizer() {
        return _loc;
    }

    @Override
    protected ClassLoader getClassLoader() {
        // we don't resolve in the context of anything but ourselves
        return getClass().getClassLoader();
    }

    protected ParsedJPQL getParsedQuery() {
        return ctx().parsed;
    }

    protected ParsedJPQL getParsedQuery(String jpql) {
        return new ParsedJPQL(jpql);
    }

    private void setCandidate(ClassMetaData cmd, String schemaAlias) {
        addAccessPath(cmd);

        if (cmd != null)
            ctx().meta = cmd;

        if (schemaAlias != null)
            ctx().schemaAlias = schemaAlias;
    }

    private String nextAlias() {
        return "jpqlalias" + (++aliasCount);
    }

    protected ClassMetaData resolveClassMetaData(JPQLNode node) {
        // handle looking up alias names
        String schemaName = assertSchemaName(node);
        ClassMetaData cmd = getClassMetaData(schemaName, false);
        if (cmd != null)
            return cmd;

        // we might be referencing a collection field of a subquery's parent
        if (isPath(node)) {
            Path path = getPath(node);
            FieldMetaData fmd = path.last();
            cmd = getFieldType(fmd);
            if (cmd == null && fmd.isElementCollection())
                cmd = fmd.getDefiningMetaData();
            return cmd;
        }

        // now run again to throw the correct exception
        return getClassMetaData(schemaName, true);
    }

    private ClassMetaData getClassMetaData(String alias, boolean assertValid) {
        ClassLoader loader = getClassLoader();
        MetaDataRepository repos = resolver.getConfiguration().
            getMetaDataRepositoryInstance();

        // first check for the alias
        ClassMetaData cmd = repos.getMetaData(alias, loader, false);

        if (cmd != null)
            return cmd;

        // now check for the class name; this is not technically permitted
        // by the JPA spec, but is required in order to be able to execute
        // JPQL queries from other facades (like JDO) that do not have
        // the concept of entity names or aliases
        Class<?> c = resolver.classForName(alias, null);
        if (c != null)
            cmd = repos.getMetaData(c, loader, assertValid);
        else if (assertValid)
            cmd = repos.getMetaData(alias, loader, false);

        if (cmd == null && assertValid) {
            String close = repos.getClosestAliasName(alias);
            if (close != null)
                throw parseException(EX_USER, "not-schema-name-hint",
                    new Object[]{ alias, close, repos.getAliasNames() }, null);
            else
                throw parseException(EX_USER, "not-schema-name",
                    new Object[]{ alias, repos.getAliasNames() }, null);
        }

        return cmd;
    }

    private Class<?> getCandidateType() {
        return getCandidateMetaData().getDescribedType();
    }

    private ClassMetaData getCandidateMetaData() {
        if (ctx().meta != null)
            return ctx().meta;

        ClassMetaData cls = getCandidateMetaData(root());
        if (cls == null)
            throw parseException(EX_USER, "not-schema-name",
                new Object[]{ root() }, null);

        setCandidate(cls, null);
        return cls;
    }

    protected ClassMetaData getCandidateMetaData(JPQLNode node) {
        // examing the node to find the candidate query
        // ### this should actually be the primary SELECT instance
        // resolved against the from variable declarations
        JPQLNode from = node.findChildByID(JJTFROMITEM, true);
        if (from == null) {
            // OPENJPA-15 allow subquery without a FROMITEM
            if (node.id == JJTSUBSELECT) {
                from = node.findChildByID(JJTFROM, true);
            }
            else {
                throw parseException(EX_USER, "no-from-clause", null, null);
            }
        }

        for (int i = 0; i < from.children.length; i++) {
            JPQLNode n = from.children[i];

            if (n.id == JJTABSTRACTSCHEMANAME) {
                // we simply return the first abstract schema child
                // as resolved into a class
                ClassMetaData cmd = resolveClassMetaData(n);

                if (cmd != null)
                    return cmd;

                // not a schema: treat it as a class
                String cls = assertSchemaName(n);
                if (cls == null)
                    throw parseException(EX_USER, "not-schema-name",
                        new Object[]{ root() }, null);

                return getClassMetaData(cls, true);
            }
            // OPENJPA-15 support subquery's from clause do not start with
            // identification_variable_declaration()
            if (node.id == JJTSUBSELECT) {
                if (n.id == JJTINNERJOIN) {
                    n = n.getChild(0);
                }
                if (n.id == JJTPATH) {
                    Path path = getPath(n);
                    FieldMetaData fmd = path.last();
                    ClassMetaData cmd = getFieldType(fmd);
                    if (cmd == null && fmd.isElementCollection())
                        cmd = fmd.getDefiningMetaData();
                    if (cmd != null) {
                        return cmd;
                    }
                    else {
                        throw parseException(EX_USER, "no-alias",
                                new Object[]{ n }, null);
                    }
                }
            }
        }

        return null;
    }

    @Override
    protected String currentQuery() {
        return ctx().parsed == null || root().parser == null ? null
            : root().parser.jpql;
    }

    QueryExpressions getQueryExpressions() {
        QueryExpressions exps = new QueryExpressions();
        exps.setContexts(contexts);

        evalQueryOperation(exps);

        Expression filter = null;
        Expression from = ctx().from;
        if (from == null)
            from = evalFromClause(root().id == JJTSELECT);
        filter = and(from, filter);
        filter = and(evalWhereClause(), filter);
        filter = and(evalSelectClause(exps), filter);

        exps.filter = filter == null ? factory.emptyExpression() : filter;

        evalGroupingClause(exps);
        evalHavingClause(exps);
        evalFetchJoins(exps);
        evalSetClause(exps);
        evalOrderingClauses(exps);

        if (parameterTypes != null)
            exps.parameterTypes = parameterTypes;

        exps.accessPath = getAccessPath();
        exps.hasInExpression = this.hasParameterizedInExpression;

        // verify parameters are consistent.
        validateParameters();

        return exps;
    }

    private Expression and(Expression e1, Expression e2) {
        return e1 == null ? e2 : e2 == null ? e1 : factory.and(e1, e2);
    }

    private static String assemble(JPQLNode node) {
        return assemble(node, ".", 0);
    }

    /**
     * Assemble the children of the specific node by appending each
     * child, separated by the delimiter.
     */
    private static String assemble(JPQLNode node, String delimiter, int last) {
        StringBuilder result = new StringBuilder();
        JPQLNode[] parts = node.children;
        for (int i = 0; parts != null && i < parts.length - last; i++)
            result.append(result.length() > 0 ? delimiter : "").
                append(parts[i].text);

        return result.toString();
    }

    private Expression assignSubselectProjection(JPQLNode node,
        QueryExpressions exps) {
        inAssignSubselectProjection = true;
        exps.projections = new Value[1];
        exps.projectionClauses = new String[1];
        exps.projectionAliases = new String[1];

        Value val = getValue(node);
        exps.projections[0] = val;
        exps.projectionClauses[0] =
            projectionClause(node.id == JJTSCALAREXPRESSION ?
                firstChild(node) : node);
        inAssignSubselectProjection = false;
        return null;
    }

    /**
     * Assign projections for NEW contructor in selection list.
     *     Example:  SELECT NEW Person(p.name) FROM Person p WHERE ...
     */
    private Expression assignProjections(JPQLNode parametersNode,
        QueryExpressions exps, List<Value> projections,
        List<String> projectionClauses, List<String> projectionAliases) {
        int count = parametersNode.getChildCount();

        Expression exp = null;
        for (int i = 0; i < count; i++) {
            JPQLNode parent = parametersNode.getChild(i);
            JPQLNode node = firstChild(parent);
            JPQLNode aliasNode = parent.children.length > 1 ? right(parent)
                : null;
            Value proj = getValue(node);
            String alias = aliasNode != null ? aliasNode.text :
                projectionClause(node.id == JJTSCALAREXPRESSION ?
                        firstChild(node) : node);
            if (aliasNode != null)
                proj.setAlias(alias);
            projections.add(proj);
            projectionClauses.add(alias);
            projectionAliases.add(alias);
        }
        return exp;
    }

    private void evalProjectionsResultShape(JPQLNode selectionsNode,
        QueryExpressions exps,
        List<Value> projections,
        List<String> projectionClauses,
        List<String> projectionAliases) {
        int count = selectionsNode.getChildCount();
        Class<?> resultClass = null;
        ResultShape<?> resultShape = null;
        if (count > 1) {
            // muti-selection
            resultClass = Object[].class;
            resultShape = new ResultShape(resultClass, new FillStrategy.Array<>(Object[].class));
        }

        for (int i = 0; i < count; i++) {
            JPQLNode parent = selectionsNode.getChild(i);
            JPQLNode node = firstChild(parent);
            if (node.id == JJTCONSTRUCTOR) {
                // build up the fully-qualified result class name by
                // appending together the components of the children
                String resultClassName = assemble(left(node));
                Class<?> constructor = resolver.classForName(resultClassName, null);
                if (constructor == null) {
                    // try resolve it again using simple name
                    int n = left(node).getChildCount();
                    String baseName = left(node).getChild(n-1).text;
                    constructor = resolver.classForName(baseName, null);
                }

                if (constructor == null && resolver.getConfiguration().getUseTCCLinSelectNew()) {
                    try {
                        if (System.getSecurityManager() != null) {
                            constructor = AccessController.doPrivileged(
                                    J2DoPrivHelper.getForNameAction(resultClassName, false,
                                        AccessController.doPrivileged(J2DoPrivHelper.getContextClassLoaderAction())));
                        }
                        else {
                            constructor = Thread.currentThread().getContextClassLoader().loadClass(resultClassName);
                        }
                    } catch (Exception e) {
                        // ignore
                    }
                }

                if (constructor == null)
                    throw parseException(EX_USER, "no-constructor",
                            new Object[]{ resultClassName }, null);

                List<Value> terms = new ArrayList<>();
                List<String> aliases = new ArrayList<>();
                List<String> clauses = new ArrayList<>();
                // now assign the arguments to the select clause as the projections
                assignProjections(right(node), exps, terms, aliases, clauses);
                FillStrategy fill = new FillStrategy.NewInstance(constructor);
                ResultShape<?> cons = new ResultShape(constructor, fill);
                for (Value val : terms) {
                    Class<?> type = val.getType();
                    cons.nest(new ResultShape(type, new FillStrategy.Assign(), type.isPrimitive()));
                }
                if (count == 1) {
                    resultClass = constructor;
                    resultShape = cons;
                }
                else
                    resultShape.nest(cons);
                projections.addAll(terms);
                projectionAliases.addAll(aliases);
                projectionClauses.addAll(clauses);

            } else {
                JPQLNode aliasNode = parent.children.length > 1 ? right(parent)
                        : null;
                Value proj = getValue(node);
                String alias = aliasNode != null ? aliasNode.text :
                    projectionClause(node.id == JJTSCALAREXPRESSION ?
                            firstChild(node) : node);
                if (aliasNode != null)
                    proj.setAlias(alias);
                projections.add(proj);
                projectionClauses.add(alias);
                projectionAliases.add(alias);
                Class<?> type = proj.getType();
                ResultShape<?> projShape = new ResultShape(type, new FillStrategy.Assign(), type.isPrimitive());

                if (count == 1)
                    resultShape = projShape;
                else
                    resultShape.nest(projShape);
            }
        }
        exps.shape = resultShape;
        exps.resultClass = resultClass;
    }

    private String projectionClause(JPQLNode node) {
        switch (node.id) {
        case JJTTYPE:
            return projectionClause(firstChild(node));
        default:
            return assemble(node);
        }
    }

    private void evalQueryOperation(QueryExpressions exps) {
        // determine whether we want to select, delete, or update
        if (root().id == JJTSELECT || root().id == JJTSUBSELECT)
            exps.operation = QueryOperations.OP_SELECT;
        else if (root().id == JJTDELETE)
            exps.operation = QueryOperations.OP_DELETE;
        else if (root().id == JJTUPDATE)
            exps.operation = QueryOperations.OP_UPDATE;
        else
            throw parseException(EX_UNSUPPORTED, "unrecognized-operation",
                new Object[]{ root() }, null);
    }

    private void evalGroupingClause(QueryExpressions exps) {
        // handle GROUP BY clauses
        JPQLNode groupByNode = root().findChildByID(JJTGROUPBY, false);

        if (groupByNode == null)
            return;

        int groupByCount = groupByNode.getChildCount();

        exps.grouping = new Value[groupByCount];

        for (int i = 0; i < groupByCount; i++) {
            JPQLNode node = groupByNode.getChild(i);
            Value val = getValue(node);
            if (val instanceof Path) {
                FieldMetaData fmd = ((Path) val).last();
                if (fmd != null && fmd.getValue().getTypeMetaData() != null && fmd.getValue().isEmbedded())
                    throw parseException(EX_USER, "cant-groupby-embeddable",
                        new Object[]{ node.getChildCount() > 1 ? assemble(node) : node.text }, null);
            }
            exps.grouping[i] = val;
        }
    }

    private void evalHavingClause(QueryExpressions exps) {
        // handle HAVING clauses
        JPQLNode havingNode = root().findChildByID(JJTHAVING, false);

        if (havingNode == null)
            return;

        exps.having = getExpression(onlyChild(havingNode));
    }

    private void evalOrderingClauses(QueryExpressions exps) {
        // handle ORDER BY clauses
        JPQLNode orderby = root().findChildByID(JJTORDERBY, false);
        if (orderby != null) {
            int ordercount = orderby.getChildCount();
            exps.ordering = new Value[ordercount];
            exps.orderingClauses = new String[ordercount];
            exps.orderingAliases = new String[ordercount];
            exps.ascending = new boolean[ordercount];
            for (int i = 0; i < ordercount; i++) {
                JPQLNode node = orderby.getChild(i);
                JPQLNode firstChild = firstChild(node);
                exps.ordering[i] = getValue(firstChild);
                exps.orderingClauses[i] = assemble(firstChild);
                exps.orderingAliases[i] = firstChild.text;

                // ommission of ASC/DESC token implies ascending
                exps.ascending[i] = node.getChildCount() <= 1 ||
                    lastChild(node).id == JJTASCENDING ? true : false;
            }
            // check if order by select item result alias
            for (int i = 0; i < ordercount; i++) {
                if (exps.orderingClauses[i] != null &&
                    !exps.orderingClauses[i].equals(""))
                    continue;
                for (int j = 0; j < exps.projections.length; j++) {
                    if (exps.projectionAliases[j].equalsIgnoreCase(
                        exps.orderingAliases[i])) {
                        exps.ordering[i] = exps.projections[j];
                        break;
                    }
                }
            }
        }
    }

    private Expression evalSelectClause(QueryExpressions exps) {
        if (exps.operation != QueryOperations.OP_SELECT)
            return null;

        JPQLNode selectNode = root();

        JPQLNode selectClause = selectNode.
            findChildByID(JJTSELECTCLAUSE, false);
        if (selectClause != null && selectClause.hasChildID(JJTDISTINCT))
            exps.distinct = QueryExpressions.DISTINCT_TRUE
                          | QueryExpressions.DISTINCT_AUTO;
        else
            exps.distinct = QueryExpressions.DISTINCT_FALSE;

        // handle SELECT clauses
        JPQLNode expNode = selectNode.
        findChildByID(JJTSELECTEXPRESSIONS, true);
        if (expNode == null) {
            return null;
        }

        int selectCount = expNode.getChildCount();
        JPQLNode selectChild = firstChild(expNode);

        if (selectClause.parent.id == JJTSUBSELECT) {
            exps.distinct &= ~QueryExpressions.DISTINCT_AUTO;
            return assignSubselectProjection(onlyChild(selectChild), exps);
        }
        // if we are selecting just one thing and that thing is the
        // schema's alias, then do not treat it as a projection
        if (selectCount == 1 && selectChild != null &&
            selectChild.getChildCount() == 1 &&
            onlyChild(selectChild) != null) {
            JPQLNode child = onlyChild(selectChild);
            if (child.id == JJTSCALAREXPRESSION)
                child = onlyChild(child);
            if (assertSchemaAlias().equalsIgnoreCase(child.text)) {
                return null;
            }
        }
        // JPQL does not filter relational joins for projections
        exps.distinct &= ~QueryExpressions.DISTINCT_AUTO;
        exps.projections = new Value[selectCount];
        List<Value> projections = new ArrayList<>();
        List<String> aliases = new ArrayList<>();
        List<String> clauses = new ArrayList<>();
        evalProjectionsResultShape(expNode, exps, projections, aliases, clauses);
        exps.projections = projections.toArray(new Value[projections.size()]);
        exps.projectionAliases = aliases.toArray(new String[aliases.size()]);
        exps.projectionClauses = clauses.toArray(new String[clauses.size()]);
        return null;
    }

    private String assertSchemaAlias() {
        String alias = ctx().schemaAlias;

        if (alias == null)
            throw parseException(EX_USER, "alias-required",
                new Object[]{ ctx().meta }, null);

        return alias;
    }

    protected Expression evalFetchJoins(QueryExpressions exps) {
        Expression filter = null;

        // handle JOIN FETCH
        Set<String> joins = null;
        Set<String> innerJoins = null;

        JPQLNode[] outers = root().findChildrenByID(JJTOUTERFETCHJOIN);
        for (int i = 0; outers != null && i < outers.length; i++)
            (joins == null ? joins = new TreeSet<>() : joins).
                add(getPath(onlyChild(outers[i])).last().getFullName(false));

        JPQLNode[] inners = root().findChildrenByID(JJTINNERFETCHJOIN);
        for (int i = 0; inners != null && i < inners.length; i++) {
            String path = getPath(onlyChild(inners[i])).last()
                .getFullName(false);
            (joins == null ? joins = new TreeSet<>() : joins).add(path);
            (innerJoins == null
                    ? innerJoins = new TreeSet<>()
                    : innerJoins).add(path);
        }

        if (joins != null)
            exps.fetchPaths = joins.
                toArray(new String[joins.size()]);
        if (innerJoins != null)
            exps.fetchInnerPaths = innerJoins.
                toArray(new String[innerJoins.size()]);

        return filter;
    }

    protected void evalSetClause(QueryExpressions exps) {
        // handle SET field = value
        JPQLNode[] nodes = root().findChildrenByID(JJTUPDATEITEM);
        for (int i = 0; nodes != null && i < nodes.length; i++) {
            Path path = getPath(firstChild(nodes[i]));
            if (path.last().getValue().getEmbeddedMetaData() != null)
                throw parseException(EX_USER, "cant-bulk-update-embeddable",
                        new Object[]{assemble(firstChild(nodes[i]))}, null);

            JPQLNode lastChild = lastChild(nodes[i]);
            Value val = (lastChild.children == null)
                      ? null : getValue(onlyChild(lastChild));
            exps.putUpdate(path, val);
        }
    }

    private Expression evalWhereClause() {
        // evaluate the WHERE clause
        JPQLNode whereNode = root().findChildByID(JJTWHERE, false);
        if (whereNode == null)
            return null;
        return (Expression) eval(whereNode);
    }

    private Expression evalFromClause(boolean needsAlias) {
        // build up the alias map in the FROM clause
        JPQLNode from = root().findChildByID(JJTFROM, false);
        if (from == null)
            throw parseException(EX_USER, "no-from-clause", null, null);
        return evalFromClause(from, needsAlias);
    }

    private Expression evalFromClause(JPQLNode from, boolean needsAlias) {
        Expression exp = null;

        for (int i = 0; i < from.children.length; i++) {
            JPQLNode node = from.children[i];

            if (node.id == JJTFROMITEM)
                exp = evalFromItem(exp, node, needsAlias);
            else if (node.id == JJTOUTERJOIN)
                exp = addJoin(node, false, exp);
            else if (node.id == JJTINNERJOIN)
                exp = addJoin(node, true, exp);
            else if (node.id == JJTINNERFETCHJOIN)
                ; // we handle inner fetch joins in the evalFetchJoins() method
            else if (node.id == JJTOUTERFETCHJOIN)
                ; // we handle outer fetch joins in the evalFetchJoins() method
            else
                throw parseException(EX_USER, "not-schema-name",
                    new Object[]{ node }, null);
        }

        return exp;
    }

    private Expression getSubquery(String alias, Path path, Expression exp) {
        Value var = getVariable(alias, true);
        // this bind is for validateMapPath to resolve alias
        Expression bindVar = factory.bindVariable(var, path);
        FieldMetaData fmd = path.last();
        ClassMetaData candidate = getFieldType(fmd);
        if (candidate == null && fmd.isElementCollection())
            candidate = fmd.getDefiningMetaData();

        setCandidate(candidate, alias);

        Context subContext = ctx();
        Subquery subquery = ctx().getSubquery();
        if (subquery == null){
            subquery = factory.newSubquery(candidate, true, alias);
            subContext.setSubquery(subquery);
        }
        else {
            subquery.setSubqAlias(alias);
        }

        Path subpath = factory.newPath(subquery);
        subpath.setSchemaAlias(path.getCorrelationVar());
        subpath.setMetaData(candidate);
        subquery.setMetaData(candidate);
        if (fmd.isElementCollection())
            exp = and(exp, bindVar);
        else
            exp = and(exp, factory.equal(path, subpath));

        return exp;
    }

    /**
     * Adds a join condition to the given expression.
     *
     * @param node the node to check
     * @param inner whether or not the join should be an inner join
     * @param exp an existing expression to AND, or null if none
     * @return the Expression with the join condition added
     */
    private Expression addJoin(JPQLNode node, boolean inner, Expression exp) {
        // the type will be the declared type for the field
        JPQLNode firstChild = firstChild(node);
        Path path = null;
        if (firstChild.id == JJTQUALIFIEDPATH)
            path = getQualifiedPath(firstChild);
        else
            path = getPath(firstChild, false, inner);

        JPQLNode alias = node.getChildCount() >= 2 ? right(node) : null;
        // OPENJPA-15 support subquery's from clause do not start with
        // identification_variable_declaration()
        if (inner && ctx().getParent() != null && ctx().schemaAlias == null) {
            return getSubquery(alias.text, path, exp);
        }

        return addJoin(path, alias, exp);
    }

    private Expression addJoin(Path path, JPQLNode aliasNode,
        Expression exp) {
        FieldMetaData fmd = path.last();

        if (fmd == null)
            throw parseException(EX_USER, "path-no-meta",
                new Object[]{ path, null }, null);

        String alias = aliasNode != null ? aliasNode.text : nextAlias();

        Value var = getVariable(alias, true);
        var.setMetaData(getFieldType(fmd));

        Expression join = null;

        // if the variable is already bound, get the var's value and
        // do a regular contains with that
        boolean bound = isBound(var);
        if (bound) {
            var = getValue(aliasNode, VAR_PATH);
        } else {
            bind(var);
            join = and(join, factory.bindVariable(var, path));
        }

        if (!fmd.isTypePC()) // multi-valued relation
        {
            if (bound)
                join = and(join, factory.contains(path, var));

            setImplicitContainsTypes(path, var, CONTAINS_TYPE_ELEMENT);
        }

        return and(exp, join);
    }

    private Expression evalFromItem(Expression exp, JPQLNode node,
        boolean needsAlias) {
        ClassMetaData cmd = resolveClassMetaData(firstChild(node));

        String alias = null;

        if (node.getChildCount() < 2) {
            if (needsAlias)
                throw parseException(EX_USER, "alias-required",
                    new Object[]{ cmd }, null);
        } else {
            alias = right(node).text;
            JPQLNode left = left(node);
            addSchemaToContext(alias, cmd);

            // check to see if the we are referring to a path in the from
            // clause, since we might be in a subquery against a collection
            if (isPath(left)) {
                Path path = getPath(left);
                return getSubquery(alias, path, exp);
            } else {
                // we have an alias: bind it as a variable
                Value var = getVariable(alias, true);
                var.setMetaData(cmd);
                bind(var);
            }
        }

        // ### we assign the first FROMITEM instance we see as
        // the global candidate, which is incorrect: we should
        // instead be mapping this to the SELECTITEM to see
        // which is the desired candidate
        if (ctx().schemaAlias == null)
            setCandidate(cmd, alias);
        else
            addAccessPath(cmd);

        return exp;
    }

    @Override
    protected boolean isDeclaredVariable(String name) {
