Package org.jpox.store.rdbms.query

Source Code of org.jpox.store.rdbms.query.JPQLQueryCompiler

/**********************************************************************
Copyright (c) 2008 Andy Jefferson and others. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Contributors:
    ...
**********************************************************************/
package org.jpox.store.rdbms.query;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

import org.jpox.ClassLoaderResolver;
import org.jpox.ObjectManager;
import org.jpox.exceptions.JPOXException;
import org.jpox.exceptions.JPOXUserException;
import org.jpox.metadata.AbstractClassMetaData;
import org.jpox.metadata.AbstractMemberMetaData;
import org.jpox.metadata.MetaDataManager;
import org.jpox.metadata.Relation;
import org.jpox.store.Extent;
import org.jpox.store.exceptions.NoSuchPersistentFieldException;
import org.jpox.store.mapped.DatastoreClass;
import org.jpox.store.mapped.DatastoreIdentifier;
import org.jpox.store.mapped.IdentifierFactory;
import org.jpox.store.mapped.MappedStoreManager;
import org.jpox.store.mapped.expression.AggregateExpression;
import org.jpox.store.mapped.expression.ArrayExpression;
import org.jpox.store.mapped.expression.BooleanExpression;
import org.jpox.store.mapped.expression.ClassExpression;
import org.jpox.store.mapped.expression.IntegerLiteral;
import org.jpox.store.mapped.expression.JoinExpression;
import org.jpox.store.mapped.expression.LogicSetExpression;
import org.jpox.store.mapped.expression.MathExpression;
import org.jpox.store.mapped.expression.NullLiteral;
import org.jpox.store.mapped.expression.QueryExpression;
import org.jpox.store.mapped.expression.ScalarExpression;
import org.jpox.store.mapped.expression.SubqueryExpression;
import org.jpox.store.mapped.expression.TemporalExpression;
import org.jpox.store.mapped.expression.UnboundVariable;
import org.jpox.store.mapped.expression.UnknownLiteral;
import org.jpox.store.mapped.expression.ScalarExpression.MethodInvocationException;
import org.jpox.store.mapped.mapping.CollectionMapping;
import org.jpox.store.mapped.mapping.JavaTypeMapping;
import org.jpox.store.mapped.mapping.MapMapping;
import org.jpox.store.mapped.mapping.MappingConsumer;
import org.jpox.store.mapped.mapping.PersistenceCapableMapping;
import org.jpox.store.mapped.query.CollectionCandidates;
import org.jpox.store.mapped.query.Queryable;
import org.jpox.store.query.AbstractJPQLQuery;
import org.jpox.store.query.AbstractJavaQuery;
import org.jpox.store.query.JPOXQueryInvalidParametersException;
import org.jpox.store.query.JPQLParser;
import org.jpox.store.query.JPQLQueryHelper;
import org.jpox.store.query.QueryCompiler;
import org.jpox.store.query.QueryCompilerSyntaxException;
import org.jpox.store.query.QueryUtils;
import org.jpox.store.query.Query.SubqueryDefinition;
import org.jpox.store.rdbms.table.CollectionTable;
import org.jpox.util.Imports;
import org.jpox.util.JPOXLogger;
import org.jpox.util.StringUtils;

/**
* Compiler of JPQL queries for RDBMS datastores.
* Takes the input query and provides two forms of compilation :-
* <ul>
* <li>preCompile - where all is compiled except that parameter values arent known</li>
* <li>executionCompile - like preCompile except also using the parameter values, just before execution</li>
* </li>
* <p>
* During either compilation step other parts of the query are resolved and are available for update
* by accessors.
* </p>
* @version $Revision$
*/
public class JPQLQueryCompiler extends JavaQueryCompiler
{
    /** Aliases encountered in FROM clause, with their information keyed by the alias string. **/
    protected transient Map aliases = new HashMap();

    /** Expressions for the candidate(s). Populated after compiking the candidates. */
    protected transient ClassExpression[] candidateExpressions;

    /** Set of parameters encountered during this compilation of the query. */
    protected transient Set processedParameters = null;

    /** Compiler for any parent query. */
    protected JPQLQueryCompiler parentCompiler = null;

    /**
     * Constructor.
     * @param query Query to compile
     * @param imports Imports handler to use for class resolution
     * @param parameters map of declared parameters in the query
     */
    public JPQLQueryCompiler(AbstractJPQLQuery query, Imports imports, Map parameters)
    {
        super(query, imports, parameters);
        this.language = "JPQL";
    }

    /**
     * Method to set the parent compiler/query that this is a subquery of.
     * Should be set before calling preCompile/executionCompile.
     * @param parentCompiler The parent compiler
     */
    public void processAsSubquery(JPQLQueryCompiler parentCompiler)
    {
        this.parentCompiler = parentCompiler;
        this.parentExpr = parentCompiler.qs;
    }

    /**
     * Perform the actual compilation of the query, populating the provided QueryExpression.
     * @param qs The QueryExpression to use during compilation
     */
    protected void performCompile(QueryExpression qs)
    {
        if (parentExpr != null)
        {
            // Compile any candidate-expression when this is a subquery
            if (subqueryCandidateExpr != null)
            {
                compileSubqueryCandidateExpression(false);
            }
        }

        // Process any candidates for required joins
        if (candidateExpressions != null)
        {
            for (int i=0;i<candidateExpressions.length;i++)
            {
                processClassExpression(candidateExpressions[i]);
            }
        }

        // Compile and apply the result/result-class to the query
        fieldExpressions.clear();
        compileResult(qs, query.getResult());
        ScalarExpression[] resultFieldExprs =
            (ScalarExpression[])fieldExpressions.toArray(new ScalarExpression[fieldExpressions.size()]);
        for (int i=0; i<resultFieldExprs.length; i++)
        {
            if (resultFieldExprs[i].getLogicSetExpression() == null)
            {
                if (resultFieldExprs[i] instanceof UnboundVariable)
                {
                    throw new JPOXUserException(LOCALISER.msg("021049",
                        ((UnboundVariable)resultFieldExprs[i]).getVariableName()));
                }
            }
            qs.crossJoin(resultFieldExprs[i].getLogicSetExpression(), true);
        }

        // Compile and apply the filter to the query
        compileFilter(qs, query.getFilter());

        // Compile and apply the grouping to the query
        ScalarExpression[] groupingFieldExprs = null;
        String grouping = query.getGrouping();
        if (grouping != null && grouping.length() > 0)
        {
            // Compile the "grouping"
            fieldExpressions.clear();
            compileGrouping(qs, grouping);
            groupingFieldExprs = (ScalarExpression[])fieldExpressions.toArray(
                new ScalarExpression[fieldExpressions.size()]);

        }

        // Compile and apply the ordering to the query
        fieldExpressions.clear();
        compileOrdering(qs, query.getOrdering());
        ScalarExpression[] orderingFieldExprs = (ScalarExpression[])fieldExpressions.toArray(
            new ScalarExpression[fieldExpressions.size()]);

        String having = query.getHaving();
        if (having != null && having.length() > 0)
        {
            compileHaving(qs, having);
        }
       
        // Check that all result expression fields, having fields and ordering fields are defined in any grouping specification
        if (groupingFieldExprs != null)
        {
            // Check that all ordering expression fields are in the grouping clause
            checkExpressionsAgainstGrouping(orderingFieldExprs, groupingFieldExprs, "021069");

            // Check that all result clause fields are in the grouping clause
            checkExpressionsAgainstGrouping(resultFieldExprs, groupingFieldExprs, "021070");
        }

        groupingFieldExprs = (ScalarExpression[])fieldExpressions.toArray(new ScalarExpression[fieldExpressions.size()]);

        // Compile and apply the range to the query
        compileRange(qs);

        // Compile the update
        compileUpdate(qs, ((JPQLQuery)query).getUpdate());

        // Check that all variables have been bound to the query
        checkVariableBinding();

        // Sanity check on implicit parameters to see if the user has set some that dont exist in the query
        if (parameters != null && parameters.size() > 0)
        {
            Set paramNames = parameters.keySet();
            Iterator iter = paramNames.iterator();
            while (iter.hasNext())
            {
                Object param = iter.next();
                if (processedParameters == null || !processedParameters.contains(param))
                {
                    throw new JPOXQueryInvalidParametersException(LOCALISER.msg("021076", param));
                }
            }
        }
    }

    /**
     * Convenience method to compile the update clause.
     * Processes the "update" and updates the QueryExpression accordingly.
     * @param qs The Query Expression to apply the update to (if specified)
     * @param update The update specification
     */
    protected void compileUpdate(QueryExpression qs, String update)
    {
        if (update != null && update.length() > 0)
        {
            ScalarExpression[] exprs = compileExpressionsFromString(update);
            if (qs != null)
            {
                qs.setUpdates(exprs);
            }
        }
    }

    /**
     * Convenience method to process the candidates for this query.
     * Processes the "candidateClassName" and "candidateClass" and sets up "candidates".
     * The QueryExpression "qs" isn't set at this point.
     */
    protected void compileCandidates()
    {
        distinct = false;
        ObjectManager om = query.getObjectManager();

        // Compile the candidate from
        compileFrom(((JPQLQuery)query).getFrom());

        // Process any result clause so we know if there is a result mapping or just the candidate
        if (query.getResult() != null)
        {
            String result = query.getResult().trim();
            if (result.toLowerCase().startsWith("distinct"))
            {
                distinct = true;
                result = result.substring(8).trim();
            }

            if (result.equalsIgnoreCase(candidateAlias))
            {
                // Just selecting candidate so remove result clause
                query.setResult(null);
            }
        }

        // Check the candidate class existence
        String candidateClassName = query.getCandidateClassName();
        if (candidateClass == null && candidateClassName != null)
        {
            try
            {
                candidateClass = om.getClassLoaderResolver().classForName(candidateClassName, true);
            }
            catch (JPOXException jpe)
            {
                candidateClass = query.resolveClassDeclaration(candidateClassName);
            }
        }

        // Set the "candidates"
        Extent candidateExtent = ((AbstractJavaQuery)query).getCandidateExtent();
        Collection candidateCollection = ((AbstractJavaQuery)query).getCandidateCollection();
        if (candidateExtent != null)
        {
            candidates = (Queryable)candidateExtent;
        }
        else if (candidateCollection != null)
        {
            candidates = new CollectionCandidates(om, candidateClass, candidateCollection);
        }
        else
        {
            if (candidateClass == null)
            {
                throw new JPOXUserException(LOCALISER.msg("021048", language));
            }
            candidates = (Queryable)om.getExtent(candidateClass, query.isSubclasses());
        }

        String result = query.getResult();
        if (result != null)
        {
            // User has specified a result so set the candidates based on the result definition
            if (candidateCollection != null)
            {
                candidates = new ResultExpressionsQueryable(om, candidateClass,
                    ((CollectionCandidates)candidates).getUserCandidates(), query.isSubclasses());
            }
            else
            {
                candidates = new ResultExpressionsQueryable(om, candidateClass, query.isSubclasses());
            }

            if (result != null && result.toLowerCase().startsWith("distinct"))
            {
                distinct = true;
            }
        }
    }

    /**
     * Compile the candidate expressions using the FROM clause.
     * Populates "candidateExpressions", "candidateClass" and "candidateAlias" as well as any aliases.
     * @param from From clause
     */
    private void compileFrom(String from)
    {
        // Compile any candidate expressions
        // Note that the query statement is null when doing this so we populate the candidateExpressions
        // and also aliases. The candidate expressions will include JoinExpressions.
        if (from != null)
        {
            // Split "from" into comma-separated expressions
            String[] exprList = QueryUtils.getExpressionsFromString(from);
            ScalarExpression[] exprs = null;
            if (exprList != null && exprList.length > 0)
            {
                exprs = new ScalarExpression[exprList.length];
                for (int i=0;i<exprs.length;i++)
                {
                    // Compile each from expression
                    exprs[i] = compileFromExpression(exprList[i]);
                }
            }

            candidateClass = ((ClassExpression)exprs[0]).getCls();
            candidateAlias = exprs[0].getAlias().toUpperCase(); // Identifiers are case insensitive

            candidateExpressions = new ClassExpression[exprs.length];
            for (int i=0; i<candidateExpressions.length; i++)
            {
                ClassExpression classExpr = (ClassExpression)exprs[i];
                if (classExpr.getCls() == null)
                {
                    // Candidate expression is for the candidate but candidate wasnt known at the time of compilation
                    candidateExpressions[i] = new ClassExpression(qs, candidateClass);
                    candidateExpressions[i].as(candidateAlias);
                    JoinExpression[] joins = classExpr.getJoins();
                    if (joins != null)
                    {
                        for (int j=0;j<joins.length;j++)
                        {
                            candidateExpressions[i].join(joins[j]);
                        }
                    }
                }
                else
                {
                    candidateExpressions[i] = classExpr;
                }
            }
        }
    }

    /**
     * Method to take an expression string from the "from" clause and convert it into a ScalarExpression.
     * The "from" clause is made up of between 1 and N expression strings (comma separated).
     * The from expression string has the form
     * <pre>{class-expression} [AS] alias [JOIN ...]</pre>
     * or
     * <pre>IN (collection-value-path) [AS] alias [JOIN ...]</pre>
     * @param fromStr The from expression string
     * @return Class Expression for this part of the FROM
     */
    protected ClassExpression compileFromExpression(String fromStr)
    {
        ClassExpression expr = null;

        p = new JPQLParser(fromStr, imports);
        if (p.parseStringIgnoreCase("IN"))
        {
            // "IN(...) [AS] alias"
            if (!p.parseChar('('))
            {
                throw new QueryCompilerSyntaxException("Expected: '(' but got " + p.remaining(),
                    p.getIndex(), p.getInput());
            }

            // Find what we are joining to
            String name = p.parseIdentifier();
            if (p.nextIsDot())
            {
                p.parseChar('.');
                name += ".";
                name += p.parseName();
            }

            if (!p.parseChar(')'))
            {
                throw new QueryCompilerSyntaxException("Expected: ')' but got " + p.remaining(),
                    p.getIndex(), p.getInput());
            }

            p.parseStringIgnoreCase("AS"); // Optional
            String alias = p.parseName();

            // Return as part of ClassExpression joining candidate class to this collection field
            expr = new ClassExpression(qs, candidateClass);
            expr.as(candidateAlias);
            JoinExpression joinExpr = new JoinExpression(qs, name, false, false);
            joinExpr.as(alias);
            expr.join(joinExpr);

            // Update with any subsequent JOIN expressions
            compileFromJoinExpressions(expr);
        }
        else
        {
            // "<candidate_expression> [AS] alias"
            String id = p.parseIdentifier();

            String name = id;
            if (p.nextIsDot())
            {
                p.parseChar('.');
                name += ".";
                name += p.parseName();
            }

            if (parentExpr != null)
            {
                // Subquery, so calculate any candidate expression
                Class cls = null;
                cls = getClassForSubqueryCandidateExpression(name);
                expr = new ClassExpression(qs, cls);
                id = p.parseIdentifier();
                if (id != null)
                {
                    // Add alias to class/class.field
                    if (id.equalsIgnoreCase("AS"))
                    {
                        id = p.parseIdentifier();
                    }
                    if (id != null)
                    {
                        expr.as(id);
                    }
                }
            }
            else
            {
                Class cls = query.resolveClassDeclaration(name);
                expr = new ClassExpression(qs, cls);
                id = p.parseIdentifier();
                if (id != null)
                {
                    // Add alias to class/class.field
                    if (id.equalsIgnoreCase("AS"))
                    {
                        id = p.parseIdentifier();
                    }
                    if (id != null)
                    {
                        expr.as(id);
                    }
                }
            }

            // Update with any subsequent JOIN expressions
            compileFromJoinExpressions(expr);
        }

        return expr;
    }

    /**
     * Method to compile the join expressions in the FROM clause, updating the input ClassExpression
     * with the discovered joins.
     * @param clsExpr Class Expression that the join(s) are applied to
     */
    protected void compileFromJoinExpressions(ClassExpression clsExpr)
    {
        boolean moreJoins = true;
        while (moreJoins)
        {
            if (clsExpr.getAlias() == null)
            {
                // Overall class expression has no alias but first part of FROM needs an alias
                // TODO Localise this and give sensible message "FROM class ... has no alias specified"
                throw new JPOXUserException("Query has missing identifier at end");
            }

            if (aliases.get(clsExpr.getAlias().toUpperCase()) == null)
            {
                // Register the class expression as an alias so we can find the next part
                AliasJoinInformation leftAliasInfo = new AliasJoinInformation(
                    clsExpr.getAlias().toUpperCase(), clsExpr.getCls(), null, true);
                aliases.put(clsExpr.getAlias().toUpperCase(), leftAliasInfo);
            }

            // Check for JOIN syntax "[LEFT [OUTER] | INNER] JOIN ..."  (EJB3 syntax)
            boolean leftJoin = false;
            boolean innerJoin = false;
            if (p.parseStringIgnoreCase("INNER"))
            {
                innerJoin = true;
            }
            else if (p.parseStringIgnoreCase("LEFT"))
            {
                //optional and useless (for parser) outer keyword
                p.parseStringIgnoreCase("OUTER");
                leftJoin = true;
            }

            if (p.parseStringIgnoreCase("JOIN"))
            {
                // Process the join
                boolean fetch = false;
                if (p.parseStringIgnoreCase("FETCH"))
                {
                    fetch = true;
                }

                // Find what we are joining to
                String id = p.parseIdentifier();
                String name = id;
                if (p.nextIsDot())
                {
                    p.parseChar('.');
                    name += ".";
                    name += p.parseName();
                }

                // And the alias we know this joined field by
                p.parseStringIgnoreCase("AS"); // Optional
                String alias = p.parseName();

                JoinExpression joinExpr = new JoinExpression(qs, name, leftJoin, fetch);
                joinExpr.as(alias);

                clsExpr.join(joinExpr);
            }
            else
            {
                if (innerJoin || leftJoin)
                {
                    throw new JPOXUserException("Expected JOIN after INNER/LEFT keyword at"+p.remaining());
                }
                moreJoins = false;
                return;
            }
        }
    }

    /**
     * Convenience method to process the subquery "<candidate-expression>" to return the class to use.
     * If the input "expression" is actually a class name then just returns the class.
     * If the input "expression" starts with the candidate alias of the parent query then
     * returns the class of the expression navigating along its tokens.
     * @param candExpr Candidate expression
     * @return Class for candidate expression
     */
    protected Class getClassForSubqueryCandidateExpression(String candExpr)
    {
        if (candExpr == null)
        {
            return null;
        }

        String[] tokens = StringUtils.split(candExpr, ".");
        Class cls = null;
        if (tokens[0].equalsIgnoreCase(parentExpr.getCandidateAlias()))
        {
            // Starts with candidate of parent query
            cls = parentExpr.getCandidateClass();
        }
        else
        {
            AliasJoinInformation aliasInfo = (AliasJoinInformation)parentCompiler.aliases.get(tokens[0].toUpperCase());
            if (aliasInfo == null)
            {
                // Not a candidate-expression so just return the class
                return query.resolveClassDeclaration(candExpr);
            }
            else
            {
                // Using joined from class of parent query
                cls = aliasInfo.cls;
                subqueryCandidateExprRootAliasInfo = aliasInfo;
            }
        }
        subqueryCandidateExpr = candExpr;

        ClassLoaderResolver clr = query.getObjectManager().getClassLoaderResolver();
        MetaDataManager mmgr = query.getObjectManager().getMetaDataManager();
        AbstractClassMetaData cmd = mmgr.getMetaDataForClass(cls, clr);
        for (int i=1;i<tokens.length;i++)
        {
            AbstractMemberMetaData mmd = cmd.getMetaDataForMember(tokens[i]);
            int relationType = mmd.getRelationType(clr);

            if (relationType == Relation.ONE_TO_ONE_BI ||
                relationType == Relation.ONE_TO_ONE_UNI ||
                relationType == Relation.MANY_TO_ONE_BI)
            {
                cls = mmd.getType();
            }
            else if (relationType == Relation.ONE_TO_MANY_UNI ||
                relationType == Relation.ONE_TO_MANY_BI ||
                relationType == Relation.MANY_TO_MANY_BI)
            {
                if (mmd.hasCollection())
                {
                    cls = clr.classForName(mmd.getCollection().getElementType());
                }
                else if (mmd.hasMap())
                {
                    // Assume we're using the value
                    cls = clr.classForName(mmd.getMap().getValueType());
                }
                else if (mmd.hasArray())
                {
                    cls = clr.classForName(mmd.getArray().getElementType());
                }
            }

            if (i < tokens.length-1)
            {
                cmd = mmgr.getMetaDataForClass(cls, clr);
            }
        }
        return cls;
    }

    /**
     * Method to process a ClassExpression from the FROM clause.
     * A ClassExpression can contain a series of joins. The main (first) ClassExpression processed will be
     * for the main candidate. Subsequent ClassExpressions can be for other classes that should be cross-joined
     * in the query. All join expressions for a ClassExpression result in either an INNER or LEFT OUTER join being
     * added to the query. See JPA spec 4.4.5.
     * <p>
     * An example of multiple class expressions
     * <pre>select c from Customer c, Employee e where c.hatsize = e.shoesize</pre>
     * so in this we have a class expression for the main candidate (Customer), and a class expression for Employee
     * which results in a cross join for Employee.
     * </p>
     * <p>
     * An example of a class expression with joins
     * <pre>
     * SELECT DISTINCT o
     * FROM Order o JOIN o.lineItems l JOIN l.product p
     * WHERE ...
     * </pre>
     * so we have a ClassExpression for Order, and this has 2 JoinExpressions within it. The first join is
     * from the field "lineItems" of Order, to class LineItem (aliased as "l"). The second join is
     * from the field "product" of LineItem, to class Product (aliased as "p").
     * </p>
     * @param classExpr The class expression to process
     */
    protected void processClassExpression(ClassExpression classExpr)
    {
        MappedStoreManager srm = (MappedStoreManager)query.getStoreManager();
        ClassLoaderResolver clr = query.getObjectManager().getClassLoaderResolver();
        JoinExpression[] joinExprs = classExpr.getJoins();
        if (classExpr.getCls() != candidateClass && classExpr.getCls() != null)
        {
            // Not candidate class so must be cross join (JPA spec 4.4.5)
            DatastoreIdentifier rightTblId =
                srm.getIdentifierFactory().newIdentifier(IdentifierFactory.TABLE, classExpr.getAlias());
            DatastoreClass rightTable = srm.getDatastoreClass(classExpr.getCls().getName(), clr);
            LogicSetExpression rightTblExpr = qs.newTableExpression(rightTable, rightTblId);
            AliasJoinInformation rightAliasInfo = new AliasJoinInformation(classExpr.getAlias().toUpperCase(),
                classExpr.getCls(), rightTblExpr, false);
            aliases.put(rightAliasInfo.alias, rightAliasInfo);
            qs.crossJoin(rightTblExpr, true);
        }

        if (joinExprs != null)
        {
            for (int i=0;i<joinExprs.length;i++)
            {
                JoinExpression joinExpr = joinExprs[i];
                String joinFieldName = joinExpr.getFieldName();

                boolean complete = false;
                int joinNum = 0;
                while (!complete)
                {
                    // Split the join field name into components so we have "alias.field"
                    // If the join field name is of the form "a.b.c.d" then this will mean multiple joins
                    int sepPos1 = joinFieldName.indexOf('.');
                    if (sepPos1 < 0)
                    {
                        // No join since no "alias.field" form
                        break;
                    }
                    int sepPos2 = joinFieldName.indexOf('.', sepPos1+1);
                    if (sepPos2 < 0)
                    {
                        complete = true;
                        sepPos2 = joinFieldName.length();
                    }

                    String leftAlias = joinFieldName.substring(0, sepPos1);
                    String fieldName = joinFieldName.substring(sepPos1+1, sepPos2);
                    String rightAlias = null;
                    if (complete)
                    {
                        rightAlias = joinExpr.getAlias();
                    }
                    else
                    {
                        rightAlias = "TMP" + joinNum;
                    }

                    if (joinExpr.getFieldName().equals(leftAlias + '.' + fieldName))
                    {
                        // No internal joins needed, so just process the compiled JoinExpression
                        processJoinExpression(joinExpr);
                    }
                    else
                    {
                        // Internal joins needed, so construct and process this component
                        JoinExpression expr = new JoinExpression(qs, leftAlias + '.' + fieldName,
                            joinExpr.isLeftJoin(), joinExpr.isFetch());
                        if (rightAlias != null)
                        {
                            expr.as(rightAlias);
                        }
                        processJoinExpression(expr);

                        if (!complete)
                        {
                            // Prepare for next internal join
                            joinNum++;
                            joinFieldName = rightAlias + '.' + joinFieldName.substring(sepPos2+1);
                        }
                    }
                }
            }
        }
    }

    /**
     * Method to process the supplied JoinExpression and add the required join to the current QueryStatement.
     * The "fieldName" of the JoinExpression must be of the form "alias.field". No multiple level
     * field names are supported here.
     * @param joinExpr JoinExpression
     */
    protected void processJoinExpression(JoinExpression joinExpr)
    {
        MappedStoreManager srm = (MappedStoreManager)query.getStoreManager();
        ClassLoaderResolver clr = query.getObjectManager().getClassLoaderResolver();
        String joinFieldName = joinExpr.getFieldName();
        if (joinFieldName.indexOf('.') < 0)
        {
            // Error!!! can only join to "{alias}.field"
        }
        else
        {
            String leftAlias = joinFieldName.substring(0, joinFieldName.indexOf('.')).toUpperCase();
            String leftFieldName = joinFieldName.substring(joinFieldName.indexOf('.')+1);
            AliasJoinInformation leftAliasInfo = (AliasJoinInformation)aliases.get(leftAlias);
            if (leftAliasInfo != null)
            {
                Class leftCls = leftAliasInfo.cls;
                DatastoreClass leftTable = srm.getDatastoreClass(leftCls.getName(), clr);
                JavaTypeMapping leftMapping = leftTable.getFieldMapping(leftFieldName);
                AbstractMemberMetaData leftMmd = leftMapping.getFieldMetaData();
                int relationType = leftMmd.getRelationType(clr);

                // TODO Generalise the table identifier use. Currently using alias, and {alias}_{alias} for joins
                String rightTblIdName = (joinExpr.getAlias() != null ? joinExpr.getAlias() : "UNKNOWN_ALIAS");
                DatastoreIdentifier rightTblId =
                    srm.getIdentifierFactory().newIdentifier(IdentifierFactory.TABLE, rightTblIdName);

                // Find table expression for left hand side of join
                LogicSetExpression leftTableExpr = leftAliasInfo.tableExpression;
                if (leftTableExpr == null)
                {
                    if (leftAlias.equalsIgnoreCase(candidateAlias)) // JPQL identifiers are case insensitive
                    {
                        // Field of candidate
                        leftTableExpr = qs.getMainTableExpression();
                        leftAliasInfo.tableExpression = leftTableExpr; // Set the table expression now we know it
                    }
                    else
                    {
                        // TODO left side is an alias of something other than the candidate
                        throw new JPOXUserException("JPOX doesnt yet support joins to non-candidate aliases");
                    }
                }

                AbstractMemberMetaData[] rightMmds = leftMmd.getRelatedMemberMetaData(clr);
                AbstractMemberMetaData rightMmd = (rightMmds != null && rightMmds.length > 0 ? rightMmds[0] : null);

                // TODO Check if right table already exists in "qs" (shouldn't since just starting "qs")
                if (leftMapping instanceof PersistenceCapableMapping)
                {
                    // 1-1, N-1 relation field
                    DatastoreClass rightTable = srm.getDatastoreClass(leftMmd.getTypeName(), clr);
                    LogicSetExpression rightTblExpr = qs.newTableExpression(rightTable, rightTblId);

                    if (relationType == Relation.ONE_TO_ONE_UNI ||
                        (relationType == Relation.ONE_TO_ONE_BI && leftMmd.getMappedBy() == null))
                    {
                        // 1-1 FK on this side [join left[FK]->right[ID])
                        ScalarExpression leftExpr = leftTableExpr.newFieldExpression(leftMmd.getName());
                        ScalarExpression rightExpr =
                            rightTable.getIDMapping().newScalarExpression(qs, rightTblExpr);
                        if (joinExpr.isLeftJoin())
                        {
                            qs.leftOuterJoin(leftExpr, rightExpr, rightTblExpr, true, true);
                        }
                        else
                        {
                            qs.innerJoin(leftExpr, rightExpr, rightTblExpr, true, true);
                        }
                    }
                    else if (relationType == Relation.ONE_TO_ONE_BI && leftMmd.getMappedBy() != null)
                    {
                        // 1-1 FK on other side [join left[ID]->right[FK])
                        ScalarExpression leftExpr =
                            leftTable.getIDMapping().newScalarExpression(qs, leftTableExpr);
                        ScalarExpression rightExpr = rightTblExpr.newFieldExpression(rightMmd.getName());
                        if (joinExpr.isLeftJoin())
                        {
                            qs.leftOuterJoin(leftExpr, rightExpr, rightTblExpr, true, true);
                        }
                        else
                        {
                            qs.innerJoin(leftExpr, rightExpr, rightTblExpr, true, true);
                        }
                    }
                    else if (relationType == Relation.MANY_TO_ONE_BI)
                    {
                        if (rightMmd.getJoinMetaData() != null || leftMmd.getJoinMetaData() != null)
                        {
                            // Join Table N-1 [join left[ID]->centre(ID_FK), centre(ID_OWN)->right[ID])
                            ScalarExpression leftExpr =
                                leftTable.getIDMapping().newScalarExpression(qs, leftTableExpr);
                            ScalarExpression rightExpr =
                                rightTable.getIDMapping().newScalarExpression(qs, rightTblExpr);
                            CollectionTable joinTbl = (CollectionTable)srm.getDatastoreContainerObject(rightMmd);
                            String joinTblIdName = rightTblIdName + "." + leftAlias;
                            DatastoreIdentifier joinTblId =
                                srm.getIdentifierFactory().newIdentifier(IdentifierFactory.TABLE, joinTblIdName);
                            LogicSetExpression joinTblExpr = qs.newTableExpression(joinTbl, joinTblId);
                            ScalarExpression joinLeftExpr =
                                joinTbl.getElementMapping().newScalarExpression(qs, joinTblExpr);
                            ScalarExpression joinRightExpr =
                                joinTbl.getOwnerMapping().newScalarExpression(qs, joinTblExpr);
                            if (joinExpr.isLeftJoin())
                            {
                                qs.leftOuterJoin(leftExpr, joinLeftExpr, joinTblExpr, true, true);
                                qs.innerJoin(joinRightExpr, rightExpr, rightTblExpr, true, true);
                            }
                            else
                            {
                                qs.innerJoin(leftExpr, joinLeftExpr, joinTblExpr, true, true);
                                qs.innerJoin(joinRightExpr, rightExpr, rightTblExpr, true, true);
                            }
                        }
                        else
                        {
                            // FK N-1 [join left[FK]->right[ID])
                            ScalarExpression leftExpr = leftTableExpr.newFieldExpression(leftMmd.getName());
                            ScalarExpression rightExpr = rightTable.getIDMapping().newScalarExpression(qs, rightTblExpr);
                            if (joinExpr.isLeftJoin())
                            {
                                qs.leftOuterJoin(leftExpr, rightExpr, rightTblExpr, true, true);
                            }
                            else
                            {
                                qs.innerJoin(leftExpr, rightExpr, rightTblExpr, true, true);
                            }
                        }
                    }
                    if (joinExpr.getAlias() != null)
                    {
                        AliasJoinInformation rightAliasInfo = new AliasJoinInformation(joinExpr.getAlias().toUpperCase(),
                            leftMmd.getType(), rightTblExpr, false);
                        aliases.put(rightAliasInfo.alias, rightAliasInfo);
                    }
                }
                else if (leftMapping instanceof CollectionMapping)
                {
                    // 1-N, M-N collection (element) field
                    DatastoreClass rightTable =
                        srm.getDatastoreClass(leftMmd.getCollection().getElementType(), clr);
                    LogicSetExpression rightTblExpr = qs.newTableExpression(rightTable, rightTblId);
                    if (relationType == Relation.MANY_TO_MANY_BI || leftMmd.getJoinMetaData() != null)
                    {
                        // TODO Cater for 1-N with join specified at other side
                        // 1-N uni/bi JoinTable relation [join left[ID]->centre(ID_OWN), centre(ID_FK)->right[ID])
                        ScalarExpression leftExpr =
                            leftTable.getIDMapping().newScalarExpression(qs, leftTableExpr);
                        ScalarExpression rightExpr =
                            rightTable.getIDMapping().newScalarExpression(qs, rightTblExpr);
                        CollectionTable joinTbl = (CollectionTable)srm.getDatastoreContainerObject(leftMmd);
                        String joinTblIdName = leftAlias + "." + rightTblIdName;
                        DatastoreIdentifier joinTblId =
                            srm.getIdentifierFactory().newIdentifier(IdentifierFactory.TABLE, joinTblIdName);
                        LogicSetExpression joinTblExpr = qs.newTableExpression(joinTbl, joinTblId);
                        ScalarExpression joinLeftExpr =
                            joinTbl.getOwnerMapping().newScalarExpression(qs, joinTblExpr);
                        ScalarExpression joinRightExpr =
                            joinTbl.getElementMapping().newScalarExpression(qs, joinTblExpr);
                        if (joinExpr.isLeftJoin())
                        {
                            qs.leftOuterJoin(leftExpr, joinLeftExpr, joinTblExpr, true, true);
                            qs.innerJoin(joinRightExpr, rightExpr, rightTblExpr, true, true);
                        }
                        else
                        {
                            qs.innerJoin(leftExpr, joinLeftExpr, joinTblExpr, true, true);
                            qs.innerJoin(joinRightExpr, rightExpr, rightTblExpr, true, true);
                        }
                    }
                    else
                    {
                        // 1-N ForeignKey relation [join left[ID]->right[FK])
                        ScalarExpression leftExpr = leftTable.getIDMapping().newScalarExpression(qs, leftTableExpr);
                        ScalarExpression rightExpr = null;
                        if (relationType == Relation.ONE_TO_MANY_UNI)
                        {
                            JavaTypeMapping m = rightTable.getExternalMapping(leftMmd, MappingConsumer.MAPPING_TYPE_EXTERNAL_FK);
                            rightExpr = m.newScalarExpression(qs, rightTblExpr);
                        }
                        else if (relationType == Relation.ONE_TO_MANY_BI)
                        {
                            rightExpr = rightTblExpr.newFieldExpression(rightMmd.getName());
                        }

                        if (joinExpr.isLeftJoin())
                        {
                            qs.leftOuterJoin(leftExpr, rightExpr, rightTblExpr, true, true);
                        }
                        else
                        {
                            qs.innerJoin(leftExpr, rightExpr, rightTblExpr, true, true);
                        }
                    }
                    if (joinExpr.getAlias() != null)
                    {
                        Class rightCls = clr.classForName(leftMmd.getCollection().getElementType());
                        AliasJoinInformation rightAliasInfo = new AliasJoinInformation(joinExpr.getAlias().toUpperCase(),
                            rightCls, rightTblExpr, false);
                        aliases.put(rightAliasInfo.alias, rightAliasInfo);
                    }
                }
                else if (leftMapping instanceof MapMapping)
                {
                    // 1-N map (value) field
                    DatastoreClass rightTable = srm.getDatastoreClass(leftMmd.getMap().getValueType(), clr);
                    if (leftMmd.getJoinMetaData() != null)
                    {
                        // JoinTable relation [join left[ID]->centre(ID_OWN), centre(ID_FK)->right[ID])
                    }
                    else
                    {
                        // ForeignKey relation [join left(ID)->right(FK)]
                    }
                    // TODO Implement Map joins
                    JPOXLogger.QUERY.debug(">> TODO 1-N (map) " +
                        "LEFT : type=" + leftMmd.getTypeName() + " mapping=" + leftMapping + " mmd=" + leftMmd +
                        "RIGHT : table=" + rightTable + " mmd=" + rightMmd);
                }
            }
        }
    }

    /**
     * Convenience method to compile the ordering defintion.
     * Processes any ordering definition and updates the QueryExpression accordingly.
     * @param qs The Query Expression to apply the ordering to (if specified)
     * @param ordering The ordering specification
     */
    protected void compileOrdering(QueryExpression qs, String ordering)
    {
        if (ordering != null && ordering.length() > 0)
        {
            // Compile the ordering
            StringTokenizer t1 = new StringTokenizer(ordering, ",");

            int n = t1.countTokens();
            ScalarExpression[] orderExprs = new ScalarExpression[n];
            boolean[] descending = new boolean[n];
            for (n = 0; t1.hasMoreTokens(); ++n)
            {
                String orderExpression = t1.nextToken().trim();

                // Allow all sensible forms of direction specification (not just JPQL standard)
                if (orderExpression.endsWith("ascending") || orderExpression.endsWith("ASCENDING"))
                {
                    descending[n] = false;
                    orderExpression = orderExpression.substring(0, orderExpression.length()-"ascending".length());
                }
                else if (orderExpression.endsWith("asc") || orderExpression.endsWith("ASC"))
                {
                    descending[n] = false;
                    orderExpression = orderExpression.substring(0, orderExpression.length()-"asc".length());
                }
                else if (orderExpression.endsWith("descending") || orderExpression.endsWith("DESCENDING"))
                {
                    descending[n] = true;
                    orderExpression = orderExpression.substring(0, orderExpression.length()-"descending".length());
                }
                else if (orderExpression.endsWith("desc") || orderExpression.endsWith("DESC"))
                {
                    descending[n] = true;
                    orderExpression = orderExpression.substring(0, orderExpression.length()-"desc".length());
                }
                else
                {
                    // Default in JPQL is ascending
                    descending[n] = false;
                }

                orderExprs[n] = compileExpressionFromString(orderExpression);
            }

            // Update the query statement
            if (qs != null)
            {
                qs.setOrdering(orderExprs, descending);
            }
        }
    }

    /**
     * Convenience method to parse an expression string into its query expression.
     * @param str The string
     * @return The query expression for the passed string
     */
    protected ScalarExpression compileExpressionFromString(String str)
    {
        try
        {
            p = new JPQLParser(str, imports);
            ScalarExpression expr = compileExpression();
            if (!p.parseEOS())
            {
                throw new QueryCompilerSyntaxException(LOCALISER.msg("021054", language), p.getIndex(), p.getInput());
            }
            return expr;
        }
        finally
        {
            p = null;
        }
    }

    /**
     * Principal method for compiling an expression.
     * An expression could be the filter, the range, the result, etc.
     * @return The compiled expression
     */
    protected ScalarExpression compileExpression()
    {
        return compileOrExpression();
        /*
         * OR ("||")
         * AND ("&&")
         * Equality ("=") ("!=")
         * Relational (">=") (">") ("<=") ("<")
         * Additive ("+") ("-")
         * Multiplicative ("*") ("/") ("%")
         * Unary ("+") ("-")
         * Unary ("~") ("!")
         * Cast
         */
    }

    protected ScalarExpression compileOrExpression()
    {
        ScalarExpression expr = compileAndExpression();
        while (p.parseStringIgnoreCase("OR"))
        {
            expr = expr.ior(compileAndExpression());
        }
        return expr;
    }

    protected ScalarExpression compileAndExpression()
    {
        ScalarExpression expr = compileNotExpression();
        while (p.parseStringIgnoreCase("AND"))
        {
            expr = expr.and(compileNotExpression());
        }
        return expr;
    }

    protected ScalarExpression compileNotExpression()
    {
        ScalarExpression expr = null;
        if (p.parseStringIgnoreCase("NOT"))
        {
            expr = compileEqualityExpression().not();
        }
        else
        {
            expr = compileEqualityExpression();
        }
        return expr;
    }

    protected ScalarExpression compileEqualityExpression()
    {
        ScalarExpression expr = compileRelationalExpression();
        for (;;)
        {
            if (p.parseString("="))
            {
                expr = expr.eq(compileRelationalExpression());
            }
            else if (p.parseStringIgnoreCase("NOT"))
            {
                if (p.parseStringIgnoreCase("BETWEEN"))
                {
                    ScalarExpression leftexpr = compileAdditiveExpression();
                    ScalarExpression rightexpr = null;
                    if (p.parseStringIgnoreCase("AND"))
                    {
                        rightexpr = compileAdditiveExpression();
                    }
                    else
                    {
                        throw new QueryCompilerSyntaxException("Expected: 'AND' but got "+p.remaining(),
                            p.getIndex(), p.getInput());
                    }
                    return expr.lt(leftexpr).ior(expr.gt(rightexpr));
                }
                else if (p.parseStringIgnoreCase("LIKE"))
                {
                    return compileLikeExpression(expr).not();
                }
                else if (p.parseStringIgnoreCase("IN"))
                {
                    return compileInExpression(expr).not();
                }
                else if (p.parseStringIgnoreCase("MEMBER"))
                {
                    return compileMemberExpression(expr).not();
                }
                throw new QueryCompilerSyntaxException("Expected: 'BETWEEN', 'LIKE', 'IN', or 'MEMBER' but got " + p.remaining(),
                    p.getIndex(), p.getInput());
            }
            else if (p.parseStringIgnoreCase("BETWEEN"))
            {
                ScalarExpression leftexpr = compileAdditiveExpression();
                ScalarExpression rightexpr = null;
                if (p.parseStringIgnoreCase("AND"))
                {
                    rightexpr = compileAdditiveExpression();
                }
                else
                {
                    throw new QueryCompilerSyntaxException("Expected: 'AND' but got "+p.remaining(),
                        p.getIndex(), p.getInput());
                }
                return expr.gteq(leftexpr).and(expr.lteq(rightexpr));
            }
            else if (p.peekStringIgnoreCase("INNER"))
            {
                // Check this before "IN" and break out if INNER (to avoid matching the "IN" check below)
                break;
            }
            else if (p.parseStringIgnoreCase("IN"))
            {
                return compileInExpression(expr);
            }
            else if (p.parseStringIgnoreCase("MEMBER"))
            {
                return compileMemberExpression(expr);
            }
            else if (p.parseStringIgnoreCase("LIKE"))
            {
                return compileLikeExpression(expr);
            }
            else if (p.parseStringIgnoreCase("IS"))
            {
                if (p.parseStringIgnoreCase("NULL"))
                {
                    expr = expr.eq(new NullLiteral(qs));
                }
                else if (p.parseStringIgnoreCase("EMPTY"))
                {
                    ArrayList args = new ArrayList();
                    expr = expr.callMethod("isEmpty",args);
                }
                else if (p.parseStringIgnoreCase("NOT"))
                {
                    if (p.parseStringIgnoreCase("NULL"))
                    {
                        expr = expr.noteq(new NullLiteral(qs));
                    }
                    else if (p.parseStringIgnoreCase("EMPTY"))
                    {
                        ArrayList args = new ArrayList();
                        expr = expr.callMethod("isEmpty",args).not();
                    }
                    else
                    {
                        throw new QueryCompilerSyntaxException("Expected: 'null' or 'empty' but got "+p.remaining(),
                            p.getIndex(), p.getInput());
                    }                   
                }
                else
                {
                    throw new QueryCompilerSyntaxException("Expected: 'null', 'empty' or 'not' but got "+p.remaining(),
                        p.getIndex(), p.getInput());
                }
            }
            else
            {
                break;
            }
        }

        return expr;
    }

    /**
     * Compile a "LIKE pattern [ESCAPE {escape char}]" expression block. The "LIKE" has already been parsed
     * before here.
     * @param expr The input expression we are performing the LIKE on
     * @return The equality expression for the LIKE
     */
    protected ScalarExpression compileLikeExpression(ScalarExpression expr)
    {
        ScalarExpression patternExpr = compileAdditiveExpression();
        if (p.parseStringIgnoreCase("ESCAPE"))
        {
            String escapeChars = p.parseStringLiteral();
            // TODO Use user-provided "escapeChars" rather than using inbuilt in datastore adapter
            // This value should likely be used in Parser when parsing the pattern expression before this
            // currently the JPQLParser allows "//" to go through ok
            JPOXLogger.QUERY.debug(">> Found escape " + escapeChars + " but not currently used by JPOX");
        }
        List list = new ArrayList();
        list.add(patternExpr);
        return expr.callMethod("like", list);
    }

    /**
     * Compile an "IN (in_item{,in_item})" expression block. The "IN" has already been parsed before here.
     * @param expr The input expression we are performing the IN on.
     * @return The equality expression for the IN
     */
    protected ScalarExpression compileInExpression(ScalarExpression expr)
    {
        ScalarExpression finalExpr = null;
        if (!p.parseChar('('))
        {
            String inStr = p.parseIdentifier();
            if (query.hasSubqueryForVariable(inStr))
            {
                // IN (subquery)
                // Subqueries wont have brackets surrounding them - removed during JPQLSingleStringParser
                SubqueryExpression inExpr = (SubqueryExpression)compileSubqueryVariable(inStr);
                return inExpr.in(expr);
            }
            else
            {
                throw new QueryCompilerSyntaxException("Expected: '(' but got " + p.remaining(),
                    p.getIndex(), p.getInput());
            }
        }

        do
        {
            // IN argument can be literal, parameter
            ScalarExpression inExpr = compilePrimary();
            if (inExpr == null)
            {
                throw new QueryCompilerSyntaxException("Expected literal|parameter but got " + p.remaining(),
                    p.getIndex(), p.getInput());
            }

            // IN ((literal|parameter) [, (literal|parameter)])
            BooleanExpression eqExpr = expr.eq(inExpr);
            if (finalExpr == null)
            {
                finalExpr = eqExpr;
            }
            else
            {
                finalExpr = finalExpr.ior(eqExpr);
            }
        } while (p.parseChar(','));

        if (!p.parseChar(')'))
        {
            throw new QueryCompilerSyntaxException("Expected: ')' but got " + p.remaining(),
                p.getIndex(), p.getInput());
        }
        return finalExpr.encloseWithInParentheses();
    }

    /**
     * Compile a "MEMBER [OF] coll_expr" expression block. The "MEMBER" has already been parsed before here.
     * @param expr The input expression we are performing the MEMBER on
     * @return The equality expression for the MEMBER
     */
    protected ScalarExpression compileMemberExpression(ScalarExpression expr)
    {
        p.parseStringIgnoreCase("OF"); // Ignore any "OF" keyword here (optional)
        ScalarExpression containerExpr = compileRelationalExpression();
        ArrayList args = new ArrayList();
        args.add(expr);
        return containerExpr.callMethod("contains", args);
    }

    protected ScalarExpression compileRelationalExpression()
    {
        ScalarExpression expr = compileAdditiveExpression();

        for (;;)
        {
            if (p.parseString("<="))
            {
                expr = expr.lteq(compileAdditiveExpression());
            }
            else if (p.parseString(">="))
            {
                expr = expr.gteq(compileAdditiveExpression());
            }
            else if (p.parseString("<>"))
            {
                expr = expr.noteq(compileAdditiveExpression());
            }
            else if (p.parseChar('<'))
            {
                expr = expr.lt(compileAdditiveExpression());
            }
            else if (p.parseChar('>'))
            {
                expr = expr.gt(compileAdditiveExpression());
            }
            else if (p.parseStringIgnoreCase("INSTANCEOF"))
            {
                // JPOX Extension not present in JPQL
                expr = expr.instanceOf(compileAdditiveExpression());
            }
            else if (p.parseStringIgnoreCase("LIKE"))
            {
                expr = compileLikeExpression(expr);
            }
            else if (p.parseStringIgnoreCase("AS"))
            {
                String asName = p.parseName();
                expr = expr.as(asName);
            }
            else
            {
                break;
            }
        }

        return expr;
    }

    /**
     * this compiles a primary. First look for a literal (e.g. "text"), then
     * an identifier(e.g. variable) In the next step, call a function, if
     * executing a function, on the literal or the identifier found.
     *
     * @return Scalar Expression for the primary compiled expression
     */
    protected ScalarExpression compilePrimary()
    {
        // try to find a literal
        ScalarExpression expr = compileLiteral();

        // if expr == null, literal not found...
        if (expr == null)
        {
            if (p.parseChar('('))
            {
                //try to find an expression
                expr = compileExpression();
                if (!p.parseChar(')'))
                {
                    throw new QueryCompilerSyntaxException("')' expected", p.getIndex(), p.getInput());
                }
                expr.encloseWithInParentheses();
            }
            else if (p.parseChar('{'))
            {
                //try to find an array
                List exprs = new ArrayList();
                while (!p.parseChar('}'))
                {
                    exprs.add(compileExpression());
                    if (p.parseChar('}'))
                    {
                        break;
                    }
                    else if (!p.parseChar(','))
                    {
                        throw new QueryCompilerSyntaxException("',' or '}' expected", p.getIndex(), p.getInput());
                    }
                }
                expr = new ArrayExpression(qs, (ScalarExpression[])exprs.toArray(new ScalarExpression[exprs.size()]));
            }
            else if (p.parseStringIgnoreCase("EXISTS"))
            {
                expr = compileIdentifier();
                if (expr instanceof SubqueryExpression)
                {
                    ((SubqueryExpression)expr).exists();
                }
                else
                {
                    throw new JPOXUserException("EXISTS can only be followed by a subquery expression");
                }
            }
            else if (p.parseStringIgnoreCase("ALL"))
            {
                expr = compileIdentifier();
                if (expr instanceof SubqueryExpression)
                {
                    ((SubqueryExpression)expr).all();
                }
                else
                {
                    throw new JPOXUserException("ALL can only be followed by a subquery expression");
                }
            }
            else if (p.parseStringIgnoreCase("ANY"))
            {
                expr = compileIdentifier();
                if (expr instanceof SubqueryExpression)
                {
                    ((SubqueryExpression)expr).any();
                }
                else
                {
                    throw new JPOXUserException("ANY can only be followed by a subquery expression");
                }
            }
            else if (p.parseStringIgnoreCase("SOME"))
            {
                expr = compileIdentifier();
                if (expr instanceof SubqueryExpression)
                {
                    ((SubqueryExpression)expr).any(); // SOME same as ANY
                }
                else
                {
                    throw new JPOXUserException("SOME can only be followed by a subquery expression");
                }
            }
            else
            {
                String methodId = p.parseMethod();
                if (methodId == null)
                {
                    // We will have an identifier (can be an variable, parameter or a field in the candidate class)
                    expr = compileIdentifier();
                }
                else
                {
                    // we are running arbitrary methods. the namespace here is "<candidateAlias>"
                    if (p.parseChar('('))
                    {
                        if (methodId.equals("TRIM"))
                        {
                            // "TRIM([[LEADING|TRAILING|BOTH] [posn] FROM] string_primary)"
                            boolean leading = true;
                            boolean trailing = true;
                            if (p.parseStringIgnoreCase("LEADING"))
                            {
                                trailing = false;
                            }
                            else if (p.parseStringIgnoreCase("TRAILING"))
                            {
                                leading = false;
                            }
                            else if (p.parseStringIgnoreCase("BOTH"))
                            {
                                // Default
                            }
                            // TODO Support trimChar
                            p.parseStringIgnoreCase("FROM"); // Ignore FROM keyword
                            ScalarExpression argExpr = compileExpression(); // The field/literal to trim
                            if (!p.parseChar(')'))
                            {
                                throw new QueryCompilerSyntaxException("')' expected", p.getIndex(), p.getInput());
                            }

                            if (leading && trailing)
                            {
                                return argExpr.callMethod("trim", Collections.EMPTY_LIST);
                            }
                            else if (leading)
                            {
                                return argExpr.callMethod("trimLeft", Collections.EMPTY_LIST);
                            }
                            else
                            {
                                return argExpr.callMethod("trimRight", Collections.EMPTY_LIST);
                            }
                        }
                        else
                        {
                            // JPQL Function expression
                            List args = new ArrayList();
                            boolean isDistinct = false;
                            if (!p.parseChar(')'))
                            {
                                // Check for use of DISTINCT (case insensitive)
                                isDistinct = p.parseStringIgnoreCase("DISTINCT");

                                do
                                {
                                    ScalarExpression argExpr = compileExpression();
                                    args.add(argExpr);
                                    fieldExpressions.remove(argExpr); // Remove from field expressions list since we will include aggregates
                                } while (p.parseChar(','));

                                if (!p.parseChar(')'))
                                {
                                    throw new QueryCompilerSyntaxException("')' expected", p.getIndex(), p.getInput());
                                }
                            }
                            MappedStoreManager srm =
                                (MappedStoreManager)(qs != null ? qs.getStoreManager() : query.getObjectManager().getStoreManager());
                            //TODO make these functions pluggable
                            if (methodId.equalsIgnoreCase("ABS"))
                            {
                                return new MathExpression(qs).absMethod(((ScalarExpression) args.get(0)));
                            }
                            else if (methodId.equalsIgnoreCase("SQRT"))
                            {
                                return new MathExpression(qs).sqrtMethod(((ScalarExpression) args.get(0)));
                            }
                            else if (methodId.equalsIgnoreCase("CONCAT"))
                            {
                                return ((ScalarExpression) args.get(0)).add((ScalarExpression) args.get(1));
                            }
                            else if (methodId.equalsIgnoreCase("MOD"))
                            {
                                return ((ScalarExpression) args.get(0)).mod((ScalarExpression) args.get(1));
                            }
                            else if (methodId.equalsIgnoreCase("LENGTH"))
                            {
                                return ((ScalarExpression) args.get(0)).callMethod(methodId.toLowerCase(),
                                    Collections.EMPTY_LIST);
                            }
                            else if (methodId.equalsIgnoreCase("SUBSTRING"))
                            {
                                List argscall = new ArrayList();
                                JavaTypeMapping mapping = srm.getDatastoreAdapter().getMapping(String.class, srm);
                                IntegerLiteral one = new IntegerLiteral(qs, mapping, BigInteger.ONE, false);
                                argscall.add(((ScalarExpression) args.get(1)).sub(one));
                                if (args.size() > 2)
                                {
                                    argscall.add(((ScalarExpression) args.get(2)).add(one));
                                }
                                return ((ScalarExpression) args.get(0)).callMethod(methodId.toLowerCase(), argscall);
                            }
                            else if (methodId.equalsIgnoreCase("LOWER"))
                            {
                                return ((ScalarExpression) args.get(0)).callMethod("toLowerCase",
                                    Collections.EMPTY_LIST);
                            }
                            else if (methodId.equalsIgnoreCase("UPPER"))
                            {
                                return ((ScalarExpression) args.get(0)).callMethod("toUpperCase",
                                    Collections.EMPTY_LIST);
                            }
                            else if (methodId.equalsIgnoreCase("SIZE"))
                            {
                                return ((ScalarExpression) args.get(0)).callMethod(methodId.toLowerCase(),
                                    Collections.EMPTY_LIST);
                            }
                            else if (methodId.equalsIgnoreCase("LOCATE"))
                            {
                                List argscall = new ArrayList();
                                argscall.add(args.get(0));
                                JavaTypeMapping mapping = srm.getDatastoreAdapter().getMapping(String.class, srm);
                                IntegerLiteral one = new IntegerLiteral(qs, mapping, BigInteger.ONE,false);
                                if (args.size() > 2)
                                {
                                    argscall.add(((ScalarExpression)args.get(2)).sub(one));
                                }
                                return ((ScalarExpression)args.get(1)).callMethod("indexOf", argscall).add(one);
                            }
                            else
                            {
                                expr = new AggregateExpression(qs);
                                if (isDistinct)
                                {
                                    ((AggregateExpression) expr).setDistinct();
                                }

                                // Allow for aggregate functions specified in lowercase/UPPERCASE
                                try
                                {
                                    expr = expr.callMethod(methodId.toLowerCase(), args);
                                    fieldExpressions.add(expr); // Remove from field exprs since we dont include aggregates
                                }
                                catch (MethodInvocationException ex)
                                {
                                    if (methodId.equalsIgnoreCase("Object"))
                                    {
                                        // Object(p)
                                        expr = (ScalarExpression) args.get(0);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        /*
         * run function on literals or identifiers
         * e.g. "primary.runMethod(arg)"
         */
        while (p.parseChar('.'))
        {
            String id = p.parseIdentifier();
            if (id == null)
            {
                throw new QueryCompilerSyntaxException("Identifier expected", p.getIndex(), p.getInput());
            }
            if (p.parseChar('('))
            {
                // Found syntax for a method, so invoke the method
                ArrayList args = new ArrayList();

                if (!p.parseChar(')'))
                {
                    do
                    {
                        args.add(compileExpression());
                    } while (p.parseChar(','));

                    if (!p.parseChar(')'))
                    {
                        throw new QueryCompilerSyntaxException("')' expected", p.getIndex(), p.getInput());
                    }
                }

                expr = expr.callMethod(id, args);
            }
            else
            {
                // access the field from an identifier
                // Why use INNER JOIN or LEFT OUTER JOIN here ? How to decide? Set to "INNER" for JPA TCK
                expr = expr.accessField(id, true);
            }
        }
        return expr;
    }

    /**
     * An identifier always designates a reference to a single value.
     * A single value can be one collection, one field.
     * @return The compiled identifier
     */
    protected ScalarExpression compileIdentifier()
    {
        String id = p.parseIdentifier();
        if (id == null)
        {
            throw new QueryCompilerSyntaxException("Identifier expected", p.getIndex(), p.getInput());
        }
        if (JPQLQueryHelper.isKeyword(id))
        {
            // Should we check on keyword or reserved identifier ?
            // Identifier is not allowed to be a JPQL keyword
            throw new QueryCompilerSyntaxException(LOCALISER.msg("021052", language, id),
                p.getIndex(), p.getInput());
        }

        MappedStoreManager srm = (MappedStoreManager)query.getObjectManager().getStoreManager();
        ClassLoaderResolver clr = query.getObjectManager().getClassLoaderResolver();
        ScalarExpression expr;

        if (id.equalsIgnoreCase("CURRENT_DATE"))
        {
            return new TemporalExpression(qs).currentDateMethod();
        }
        else if (id.equalsIgnoreCase("CURRENT_TIME"))
        {
            return new TemporalExpression(qs).currentTimeMethod();
        }
        else if (id.equalsIgnoreCase("CURRENT_TIMESTAMP"))
        {
            return new TemporalExpression(qs).currentTimestampMethod();
        }
        else if (id.equalsIgnoreCase("true"))
        {
            JavaTypeMapping m = srm.getDatastoreAdapter().getMapping(Boolean.class, srm, clr);
            return m.newLiteral(qs, Boolean.TRUE);
        }
        else if (id.equalsIgnoreCase("false"))
        {
            JavaTypeMapping m = srm.getDatastoreAdapter().getMapping(Boolean.class, srm, clr);
            return m.newLiteral(qs, Boolean.FALSE);
        }
        else if (id.startsWith(":"))
        {
            // Named parameters (":param1")
            return compileNamedImplicitParameter(id);
        }
        else if (id.startsWith("?"))
        {
            // Numbered parameters ("?1")
            return compileNumberedImplicitParameter(id);
        }
        else if (id.equalsIgnoreCase("NEW"))
        {
            // Found syntax for "new MyObject(param1, param2)" - result expression (JPA1 $4.8.2]
            return compileNewObject();
        }
        else if (query.hasSubqueryForVariable(id))
        {
            // Variable represented as a subquery (process before explicit variables below)
            return compileSubqueryVariable(id);
        }
        else if (variableNames.contains(id))
        {
            // Identifier is an explicit variable
            return compileExplicitVariable(id);
        }
        else
        {
            try
            {
                // Check if the identifier is a field of the candidate class
                expr = qs.getMainTableExpression().newFieldExpression(id);

                // What is this doing ? If "id" is a field then it comes to this, so how can it be an alias too ?
                if (!id.equalsIgnoreCase(candidateAlias)) // JPQL identifiers are case insensitive
                {
                    // Make a check on whether the field exists in the candidate class (TableExpression only checks the same table).
                    if (candidateCmd == null)
                    {
                        candidateCmd = query.getObjectManager().getMetaDataManager().getMetaDataForClass(
                            candidateClass, clr);
                    }
                    if (candidateCmd.getMetaDataForMember(id) == null)
                    {
                        // The field doesnt exist in the candidate class, so no point proceeding
                        throw new JPOXUserException(LOCALISER.msg("021049", id));
                    }
                }

                fieldExpressions.add(expr); // Add to the field expressions list
            }
            catch (NoSuchPersistentFieldException nspfe)
            {
                // Not a field of the candidate class, so check if an alias (case insensitive)
                AliasJoinInformation aliasInfo = (AliasJoinInformation)aliases.get(id.toUpperCase());
                if (aliasInfo == null && parentCompiler != null)
                {
                    // This is a subquery, so try the parent query
                    aliasInfo = (AliasJoinInformation)parentCompiler.aliases.get(id.toUpperCase());
                }
                if (aliasInfo != null)
                {
                    DatastoreClass table = srm.getDatastoreClass(aliasInfo.cls.getName(), clr);
                    if (aliasInfo.tableExpression == null)
                    {
                        if (aliasInfo.alias.equalsIgnoreCase(candidateAlias))
                        {
                            aliasInfo.tableExpression = qs.getMainTableExpression();
                        }
                        else
                        {
                            DatastoreIdentifier tableId =
                                srm.getIdentifierFactory().newIdentifier(IdentifierFactory.TABLE, aliasInfo.alias);
                            aliasInfo.tableExpression = qs.newTableExpression(table, tableId);
                        }
                    }
                    return table.getIDMapping().newScalarExpression(qs, aliasInfo.tableExpression);
                }

                // Not an alias so retrieve the full name and it is either a class, or an implicit variable
                String name = id;
                if (p.nextIsDot())
                {
                    p.parseChar('.');
                    name += ".";
                    name += p.parseName();
                }

                Class cls = null;
                try
                {
                    cls = query.resolveClassDeclaration(name);
                    expr = new ClassExpression(qs, cls);
                }
                catch (JPOXUserException jdoe)
                {
                    if (aliasInfo == null && name.indexOf('.') > 0)
                    {
                        // Try without the last part of the name (in case its a class name plus field or method)
                        String partialName = name.substring(0, name.lastIndexOf('.'));
                        String finalNamePart = name.substring(name.lastIndexOf('.')+1);
                        try
                        {
                            //try method invocation
                            expr = callUserDefinedScalarExpression(name);
                            if (expr == null)
                            {
                                cls = query.resolveClassDeclaration(partialName);
                                //try field access
                                expr = new ClassExpression(qs, cls);
                                expr = expr.accessField(finalNamePart, true);
                            }
                        }
                        catch (JPOXUserException jdoe2)
                        {
                            throw new JPOXUserException(LOCALISER.msg("021066", partialName),jdoe2);
                        }
                    }
                    else
                    {
                        try
                        {
                            // try with candidate class using field access
                            expr = new ClassExpression(qs, candidateClass);
                            expr = expr.accessField(name, true);
                        }
                        catch (JPOXUserException jdoe2)
                        {
                            // Implicit variable
                            expr = (ScalarExpression)expressionsByVariableName.get(name);
                            if (expr == null)
                            {
                                // Type will be null here since we have no types specified for implicit variables
                                // The type will be set later when we know its context
                                expr = new UnboundVariable(qs, name, aliasInfo.cls, this);
                                variableNames.add(name);
                                // We should really add this to variableTypesByName
                                fieldExpressions.add(expr); // Add to the field expressions list
                            }
                        }
                    }
                }
            }
        }

        return expr;
    }

    /**
     * Method to compile a subquery, replacing the specified variable with a SubqueryExpression.
     * @param id Variable name that the subquery replaces.
     * @return Expression for the subquery
     */
    protected ScalarExpression compileSubqueryVariable(String id)
    {
        SubqueryDefinition subqueryDef = query.getSubqueryForVariable(id);

        // Compile the subquery to get our statement
        JPQLQueryCompiler subCompiler = new JPQLQueryCompiler((AbstractJPQLQuery)subqueryDef.getQuery(),
            imports, parameters);
        subCompiler.processAsSubquery(this);
        QueryExpression subqueryExpr = (QueryExpression)subCompiler.compile(QueryCompiler.COMPILE_EXECUTION);

        // Make sure the result clause is added - this should be refactored into the Compiler from newROF()
        subCompiler.getCandidates().newResultObjectFactory(subqueryExpr, false, subCompiler.getResultClass(), true);

        ScalarExpression expr = new SubqueryExpression(qs, subqueryExpr);

        // Mark all subquery processed parameters as processed for this query too
        Set subProcessedParams = subCompiler.processedParameters;
        if (subProcessedParams != null)
        {
            Iterator iter = subProcessedParams.iterator();
            while (iter.hasNext())
            {
                String param = (String)iter.next();
                if (processedParameters == null)
                {
                    processedParameters = new HashSet();
                }
                this.processedParameters.add(param);
            }
        }

        // Mark the variable as bound
        expressionsByVariableName.put(id, expr);

        return expr;
    }

    /**
     * Method to compile a named implicit parameter into an expression.
     * @param id Identifier of the named parameter, starts with ":"
     * @return Expression representing the named param
     */
    protected ScalarExpression compileNamedImplicitParameter(String id)
    {
        MappedStoreManager srm = (MappedStoreManager)query.getObjectManager().getStoreManager();
        ClassLoaderResolver clr = query.getObjectManager().getClassLoaderResolver();
        id = id.substring(1); // Omit the ":"
        if (processedParameters == null)
        {
            processedParameters = new HashSet();
        }
        processedParameters.add(id); // Register as processed for this query

        if (parameters != null && parameters.size() > 0)
        {
            if (parameters.containsKey(id))
            {
                // Implicit parameter defined, so use the value
                Object paramValue = parameters.get(id);
                if (paramValue != null)
                {
                    JavaTypeMapping m = srm.getDatastoreAdapter().getMapping(paramValue.getClass(), srm, clr);
                    ScalarExpression paramExpr = m.newLiteral(qs, paramValue);
                    paramExpr.setParameterName(id);
                    paramExpr.checkForTypeAssignability();
                    return paramExpr;
                }
                else
                {
                    return new NullLiteral(qs);
                }
            }

            if (!executionCompile)
            {
                // No value for the current (implicit) parameter so put a null for it for now
                return new UnknownLiteral(qs);
            }
            else
            {
                // No parameter value defined for this named parameter so throw Exception
                throw new JPOXUserException(LOCALISER.msg("021075", id));
            }
        }
        else
        {
            // No parameter value defined for this named parameter so throw Exception
            throw new JPOXUserException(LOCALISER.msg("021075", id));
        }
    }

    /**
     * Method to compile a numbered implicit parameter into an expression.
     * @param id Identifier of the named parameter, starts with "?" followed by the number
     * @return Expression representing the numbered param
     */
    protected ScalarExpression compileNumberedImplicitParameter(String id)
    {
        MappedStoreManager srm = (MappedStoreManager)query.getObjectManager().getStoreManager();
        ClassLoaderResolver clr = query.getObjectManager().getClassLoaderResolver();
        id = id.substring(1); // Omit the "?"
        Integer position = new Integer(id);
        if (processedParameters == null)
        {
            processedParameters = new HashSet();
        }
        processedParameters.add(position); // Register as processed for this query

        if (parameters != null && parameters.size() > 0)
        {
            if (parameters.containsKey(position))
            {
                // Implicit parameter already found, so reuse the value
                Object paramValue = parameters.get(position);
                if (paramValue != null)
                {
                    JavaTypeMapping m = srm.getDatastoreAdapter().getMapping(paramValue.getClass(), srm, clr);
                    ScalarExpression paramExpr = m.newLiteral(qs, paramValue);
                    paramExpr.setParameterName(id);
                    paramExpr.checkForTypeAssignability();
                    return paramExpr;
                }
                else
                {
                    return new NullLiteral(qs);
                }
            }

            if (!executionCompile)
            {
                // No value for the current (implicit) parameter so put a null for it for now
                return new UnknownLiteral(qs);
            }
            else
            {
                // No parameter value defined for this numbered parameter so throw Exception
                throw new JPOXUserException(LOCALISER.msg("021075", "" + position));
            }
        }
        else
        {
            // No parameter value defined for this numbered parameter so throw Exception
            throw new JPOXUserException(LOCALISER.msg("021075", "" + position));
        }
    }
}
TOP

Related Classes of org.jpox.store.rdbms.query.JPQLQueryCompiler

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.