Package com.force.sdk.jpa.query

Source Code of com.force.sdk.jpa.query.ForceQueryUtils$ExpressionMetaData

/**
* Copyright (c) 2011, salesforce.com, inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided
* that the following conditions are met:
*
*    Redistributions of source code must retain the above copyright notice, this list of conditions and the
*    following disclaimer.
*
*    Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
*    the following disclaimer in the documentation and/or other materials provided with the distribution.
*
*    Neither the name of salesforce.com, inc. nor the names of its contributors may be used to endorse or
*    promote products derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/

package com.force.sdk.jpa.query;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URL;
import java.sql.SQLException;
import java.util.*;

import javax.jdo.identity.StringIdentity;
import javax.jdo.spi.PersistenceCapable;

import org.datanucleus.ClassLoaderResolver;
import org.datanucleus.FetchPlan;
import org.datanucleus.exceptions.*;
import org.datanucleus.metadata.*;
import org.datanucleus.query.compiler.QueryCompilation;
import org.datanucleus.query.evaluator.JDOQLResultClassMapper;
import org.datanucleus.query.expression.*;
import org.datanucleus.query.expression.Expression.MonadicOperator;
import org.datanucleus.query.symbol.Symbol;
import org.datanucleus.store.*;
import org.datanucleus.store.query.AbstractJavaQuery;
import org.datanucleus.store.query.Query;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.force.sdk.jpa.*;
import com.force.sdk.jpa.annotation.JoinFilter;
import com.force.sdk.jpa.exception.ForceApiExceptionMap;
import com.force.sdk.jpa.query.formatter.MultiPicklistFormatter;
import com.force.sdk.jpa.table.*;
import com.sforce.soap.partner.*;
import com.sforce.soap.partner.fault.ApiFault;
import com.sforce.soap.partner.sobject.SObject;
import com.sforce.ws.ConnectionException;
import com.sforce.ws.bind.XmlObject;

/**
*
* This class contains the bulk of the logic for building SOQL queries based on
* the Java query object.
*
* @author Fiaz Hossain
*/
public class ForceQueryUtils {

    static final Logger LOGGER = LoggerFactory.getLogger("com.force.sdk.jpa.query");
   
    private static final int MAX_EXPRESSION_DEPTH = 100;
    private static final int MAX_DELETE_IDS = 200;
    private static final Set<String> AGGREGATE_METHODS;
    private static final Set<String> UNSUPPORTED_JOINS;
   
    static {
        HashSet<String> am = new HashSet<String>();
        am.add("COUNT");
        am.add("SUM");
        am.add("AVG");
        am.add("MIN");
        am.add("MAX");
        AGGREGATE_METHODS = Collections.unmodifiableSet(am);
       
        // Unsupported Joins
        HashSet<String> uj = new HashSet<String>();
        uj.add("ALL");
        uj.add("ANY");
        uj.add("SOME");
        UNSUPPORTED_JOINS = Collections.unmodifiableSet(uj);
    }

    /**
     * limit type used for determining how the JPA query is executed.
     */
    enum LimitType {
        None,
        Soql,
        Java
    }
   
    private ExecutionContext ec;
    private ForceManagedConnection mconn;
    private AbstractJavaQuery query;
    private Map<Object, Object> parameters;
    private Map<String, QueryListener> listeners;
    private Map<String, Object> hints;
    private int currentHint;
  
    /**
     * Creates the query util for a specific query.
     *
     * @param ec the ExecutionContext
     * @param mconn the managed connection to connect to Force.com
     * @param query - parsed query
     * @param parameters - query parameters
     * @param listeners - query listeners
     * @param hints - query hints
     */
    public ForceQueryUtils(ExecutionContext ec, ForceManagedConnection mconn,
            AbstractJavaQuery query, Map<Object, Object> parameters,
            Map<String, QueryListener> listeners, Map<String, Object> hints) {
        this.ec = ec;
        this.mconn = mconn;
        this.query = query;
        this.parameters = parameters;
        this.listeners = listeners;
        this.hints = hints;
        this.currentHint = 0;
    }
   
    /**
     * See if there is a limit set and if we can use SOQL to execute the query.
     *
     * @param query Query
     * @return the limit type of Soql, Java, or None
     */
    public static LimitType getLimitType(Query query) {
        if (query.getRangeFromIncl() > 0 || (query.getRangeToExcl() > 0 && query.getRangeToExcl() < Long.MAX_VALUE)) {
            if (query.getRangeFromIncl() == 0) return LimitType.Soql;
            return LimitType.Java;
        }
        return LimitType.None;
    }
   
    ExecutionContext getExecutionContext() {
        return ec;
    }
   
    ForceManagedConnection getManagedConnection() {
        return mconn;
    }
   
    Object getHints(String hint) {
        return hints != null ? hints.get(hint) : null;
    }
    /**
     *
     * Convenience method to delete all objects of the candidate type from the
     * specified XML connection.
     *
     * @param candidateClass Candidate
     * @return Number of objects deleted
     */
    Long deleteObjectsOfCandidateType(Object candidateClass) {
        try {
            AbstractClassMetaData acmd =
                candidateClass instanceof String ? ec.getMetaDataManager().getMetaDataForEntityName((String) candidateClass)
                : ec.getMetaDataManager().getMetaDataForClass((Class) candidateClass, ec.getClassLoaderResolver());
            if (acmd == null) {
                throw new NucleusUserException("Entity not found: " + candidateClass);
            }
            final ForceStoreManager storeManager = (ForceStoreManager) ec.getStoreManager();

            TableImpl table = storeManager.getTable(acmd);
            PartnerConnection service = (PartnerConnection) mconn.getConnection();

            long totalDeleted = 0;
            boolean done = false;
            while (!done) {
                QueryResult qr =
                    service.query(buildQuery(table, acmd, null, query.getCompilation(), false,
                                                MAX_DELETE_IDS, null, table.getTableName().getForceApiName()));
                if (qr.getSize() == 0) return totalDeleted;
                String[] idsToDelete = new String[qr.getSize()];
                for (int i = 0; i < idsToDelete.length; i++) {
                    idsToDelete[i] = qr.getRecords()[i].getId();
                }
                // Now bulk delete them
                DeleteResult[] deleteResult = service.delete(idsToDelete);
                ForcePersistenceHandler.checkForErrors(deleteResult);
               
                // If all is well check for "EmptyRecycleBin" hint
                if (hints != null) {
                    if (Boolean.TRUE.equals(hints.get(QueryHints.EMPTY_RECYCLE_BIN))) {
                        EmptyRecycleBinResult[] emptyResult = service.emptyRecycleBin(idsToDelete);
                        ForcePersistenceHandler.checkForRecycleBinErrors(emptyResult);
                    }
                }
                totalDeleted += deleteResult.length;
                if (deleteResult.length < MAX_DELETE_IDS) {
                    return totalDeleted;
                }
            }
            return totalDeleted;
        } catch (ApiFault af) {
            throw new NucleusDataStoreException(af.toString(), af);
        } catch (Exception e) {
            throw new NucleusDataStoreException(e.getMessage(), e);
        }
    }

    /**
     * Convenience method to get all objects of the candidate type (and optional subclasses) from the
     * specified XML connection.
     *
     * @param ec ObjectManager
     * @param mconn WSC connection
     * @param candidateClass Candidate
     * @param query - parsed query
     * @return List of objects of the candidate type (or subclass)
     */
    List<Object> getObjectsOfCandidateType(Expression[] resultExpr) {
        try {
            ForceStoreManager storeManager = (ForceStoreManager) ec.getStoreManager();

            ClassLoaderResolver clr = ec.getClassLoaderResolver();
            AbstractClassMetaData acmd = null;
            TableImpl table = null;
            /**
             * - If there is a candidate class and result class we are in JPQL and give result class the upper hand
             * - If result class can be mapped to a table use it.
             *       Otherwise result class must be a non-persistence capable class
             * - If there is no candidate class but a result class it can be JPQL or NativeQuery.
             *       Either way, use that class.
             */
            if (query.getCandidateClass() != null) {
                AbstractClassMetaData candidateCmd = ec.getMetaDataManager().getMetaDataForClass(query.getCandidateClass(), clr);
                if (candidateCmd == null) {
                    throw new NucleusUserException("Candidate entity not found: " + query.getCandidateClass());
                }
                TableImpl candidateTable = storeManager.getTable(candidateCmd);
                if (query.getResultClass() != null) {
                   /**
                    * Validate that Result class is compatible with the candidate class. This can happen if two entities are
                    * mapped to the same standard/custom object in Force.com
                    */
                    acmd = ec.getMetaDataManager().getMetaDataForClass(query.getResultClass(), clr);
                    if (acmd != null) {
                        TableImpl resultTable = storeManager.getTable(acmd);
                        if (!resultTable.getTableName().getForceApiName()
                                .equals(candidateTable.getTableName().getForceApiName())) {
                            // The data is incompatible and cannot be saved to this entity
                            throw new NucleusUserException(
                                    String.format("Result class: %s is not compatible with force.com table: %s",
                                                    query.getResultClass().getName(), candidateTable.getTableName().getName()));
                        }
                        table = resultTable;
                    }
                }
                if (acmd == null) {
                   acmd = candidateCmd;
                   table = candidateTable;
                }
            } else if (query.getResultClass() != null) {
                acmd = ec.getMetaDataManager().getMetaDataForClass(query.getResultClass(), clr);
            }
           
            PartnerConnection service = (PartnerConnection) mconn.getConnection();

            QueryResult qr;
            Set<Integer> fieldsToLoad;
            int [] fieldsLoaded;
            if (query.getCompilation() != null && acmd != null) {
                // This is used for JDOQL and JPQL
                fieldsToLoad = getFieldsToLoad(acmd, query.getFetchPlan());
                String soqlQuery =
                    buildQuery(table, acmd, fieldsToLoad, query.getCompilation(), false,
                                query.getRangeToExcl(), query.getFetchPlan(), table.getTableName().getForceApiName());
                if (LOGGER.isDebugEnabled()) {
                    StringBuilder sb = new StringBuilder(soqlQuery.length() * 2);
                    sb.append("Executing JPQL: " + query.getSingleStringQuery()).append("\n").append("SOQL: ").append(soqlQuery);
                    LOGGER.debug(sb.toString());
                }
                qr = service.query(soqlQuery);
                fieldsLoaded = new int[fieldsToLoad.size()];
                int i = 0;
                for (int f : fieldsToLoad) {
                    fieldsLoaded[i++] = f;
                }
            } else {
                // This is used for Native query
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Executing Native SOQL: " + query.getSingleStringQuery());
                }
                qr = service.query(query.getSingleStringQuery());
                fieldsLoaded = acmd != null ? acmd.getDFGMemberPositions() : null;
            }
            return getResultFromQueryResult(acmd, fieldsLoaded, qr, resultExpr);
        } catch (ApiFault af) {
            throw ForceApiExceptionMap.mapToNucleusException(af, true /* isQuery */,
                    ((ForceStoreManager) ec.getStoreManager()).isEnableOptimisticTransactions());
        } catch (Exception e) {
            throw new NucleusDataStoreException(e.getMessage(), e);
        }
    }
   
    private Set<Integer> getFieldsToLoad(AbstractClassMetaData acmd, FetchPlan fetchPlan) {
        Set<Integer> memberPositions;
        if (fetchPlan != null && fetchPlan.getGroups().size() > 1) {
            memberPositions = new LinkedHashSet<Integer>(acmd.getMemberCount());
            for (String group : fetchPlan.getGroups()) {
                if ("default".equals(group)) {
                    for (int i : acmd.getDFGMemberPositions()) {
                        memberPositions.add(i);
                    }
                } else {
                    FetchGroupMetaData fgmd = acmd.getFetchGroupMetaData(group);
                    if (fgmd == null) {
                        throw new NucleusDataStoreException("Fetch group metadata not found for group: " + group);
                    }
                    addFetchGroupMemberPositions(acmd, memberPositions, fgmd);
                }
            }
        } else {
            memberPositions = new LinkedHashSet<Integer>(acmd.getDFGMemberPositions().length);
            for (int pos : acmd.getDFGMemberPositions()) {
                memberPositions.add(pos);
            }
        }
        return memberPositions;
    }
   
    private void addFetchGroupMemberPositions(AbstractClassMetaData acmd, Set<Integer> memberPositions, FetchGroupMetaData fm) {
        for (AbstractMemberMetaData ammd : fm.getMemberMetaData()) {
            memberPositions.add(ammd.getAbsoluteFieldNumber());
        }
        for (FetchGroupMetaData f : fm.getFetchGroupMetaData()) {
            addFetchGroupMemberPositions(acmd, memberPositions, f);
        }
    }
   
    private List<Object> getResultFromQueryResult(final AbstractClassMetaData acmd, final int[] fieldsToLoad,
            QueryResult qr, Expression[] resultExpr) throws ConnectionException, SQLException {
        List<Object> results = new ArrayList<Object>();
        final ClassLoaderResolver clr = ec.getClassLoaderResolver();
        final ForceStoreManager storeManager = (ForceStoreManager) ec.getStoreManager();
        /**
         * Here is how we send the data back -
         *
         * - No candidate class and no result class - the result will be a List of Objects
         *   (when there is a single column in the query), or a List of Object[]s (when there are multiple columns in the query)
         * - Candidate class specified, no result class - the result will be a List of candidate class objects,
         *   or will be a single candidate class object (when you have specified "unique").
         *   The columns of the query's result set are matched up to the fields of the candidate class by name.
         *   You need to select a minimum of the PK columns in the SQL statement.
         * - No candidate class, result class specified - the result will be a List of result class objects,
         *   or will be a single result class object (when you have specified "unique").
         *   Your result class has to abide by the rules of JDO2 result classes (see Result Class specification)
         *   This typically means either providing public fields matching
         *   the columns of the result, or providing setters/getters for the columns of the result.
         * - Candidate class and result class specified - the result will be a List of result class objects,
         *   or will be a single result class object (when you have specified "unique").
         *   The result class has to abide by the rules of JDO2 result classes (see Result Class specification).
         */
        if (resultExpr != null || (acmd == null && query.getResultClass() != null)) {
            /**
             * This section is for scalar, aggregate queries, group by, having etc.
             */
            if (qr.getRecords().length > 0) {
                results.addAll(readNonEntityObjects(qr.getRecords(), resultExpr, query.getResultClass()));
            } else if (resultExpr.length == 1 && resultExpr[0] instanceof InvokeExpression
                        && "COUNT".equals(((InvokeExpression) resultExpr[0]).getOperation())) {
                // Typically means select count() or no rows found.
                // For count we return a long value for no rows found we just return empty result
                results.add((long) qr.getSize());
            }
        } else if (fieldsToLoad == null) {
            // This is for native queries only.
            // Where we do not specify return object type. We just return the SObjects in that case
            if (query instanceof SOQLQuery && ((SOQLQuery) query).getResultMetaData() != null) {
                // Do resultset processing
                ResultMetaDataROF rof = new ResultMetaDataROF(((SOQLQuery) query).getResultMetaData());
                for (final SObject sobject : qr.getRecords()) {
                    results.add(rof.getObject(ec, mconn, query, sobject));
                }
            } else {
                Collections.addAll(results, qr.getRecords());
            }
        } else {
            for (final SObject sobject : qr.getRecords()) {
                results.add(ec.findObjectUsingAID(new Type(clr.classForName(acmd.getFullClassName())),
                                getFieldValues2(acmd, fieldsToLoad, mconn, storeManager, sobject, query),
                                query.getIgnoreCache(), true));
            }
        }
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Result raw rows: " + qr.getRecords().length + " processed rows: " + results.size());
        }
        return results;
    }

    /**
     * Constructs the proper interface for fetching fields on sobjects.
     *
     * @param acmd  the class metadata for the entity being queried
     * @param fieldsToLoad  the fields selected by the query (or selected by the fetch group)
     * @param mconn  the managed connection to Force.com APIs
     * @param storeManager  the store manager
     * @param sobject the sobject being queried
     * @param query the query for retrieving objects
     * @return  an interface for retrieving field values
     */
    public static FieldValues2 getFieldValues2(
            final AbstractClassMetaData acmd, final int[] fieldsToLoad,
            final ForceManagedConnection mconn,
            final ForceStoreManager storeManager, final XmlObject sobject, final Query query) {
        return new FieldValues2() {
            // StateManager calls the fetchFields method
            @Override
            public void fetchFields(ObjectProvider sm) {
                try {
                    sm.replaceFields(acmd.getPKMemberPositions(),
                                        new ForceFetchFieldManager(sm, storeManager, mconn, sobject, query));
                    sm.replaceFields(fieldsToLoad, new ForceFetchFieldManager(sm, storeManager, mconn, sobject, query));
                } catch (Exception e) {
                    throw new NucleusException(e.getMessage(), e);
                }
            }

            @Override
            public void fetchNonLoadedFields(ObjectProvider sm) {
                try {
                    sm.replaceNonLoadedFields(acmd.getAllMemberPositions(),
                                                new ForceFetchFieldManager(sm, storeManager, mconn, sobject, query));
                } catch (Exception e) {
                    throw new NucleusException(e.getMessage(), e);
                }
            }

            @Override
            public FetchPlan getFetchPlanForLoading() {
                return null;
            }
        };
    }
   
    private void getFieldNameList(SObject[] sObjects, boolean createFieldNameExpressions,
            List<String> fieldNameList, List<Expression> fieldNameExprs) {
        Iterator<XmlObject> fieldIter = sObjects[0].getChildren();
        /**
         * Skip the first two items. First item is "type" and second is "id".
         * If we have selected id then it will show up again in the list.
         */
        for (int i = 0; fieldIter.hasNext(); i++) {
            XmlObject xo = fieldIter.next();
            String name = xo.getName().getLocalPart();
            if (i < 2) continue;
            fieldNameList.add(name);
            if (createFieldNameExpressions) {
                fieldNameExprs.add(new PrimaryExpression(Arrays.asList(name)));
            }
        }
    }
   
    private Collection<Object> readExpressionObjects(SObject[] sObjects, Expression[] exprs, List<String> fieldNameList)
        throws ConnectionException, SQLException {
       
        List<Object> res = new ArrayList<Object>(sObjects.length);
        Map<String, ForceFetchFieldManager> ffms = new HashMap<String, ForceFetchFieldManager>();
        for (SObject sObject : sObjects) {
            ffms.clear();
            List<Object> row = new ArrayList<Object>(exprs.length);
            for (int i = 0; i < exprs.length; i++) {
                row.add(getDataForExpression(ffms, sObject, exprs[i], fieldNameList.get(i), null));
            }
            res.add(row.size() > 1 ? row.toArray() : row.get(0));
        }
        return res;
    }

    private Collection<Object> readCreatorExpressionObjects(Class clazz, SObject[] sObjects, Expression[] exprs,
            List<String> fieldNameList) throws ConnectionException, SQLException {
        Collection<Object> res = readExpressionObjects(sObjects, exprs, fieldNameList);
        return new JDOQLResultClassMapper(clazz).map(res, toShortNameExpressions(exprs));
    }

    /**
     * Given a list of expressions in a.b.c format return a list of expressions that are only field names, 'c' in this example.
     * @param exprs - expressions to convert
     * @return - returns an array of expressions that only contain the field name
     */
    private Expression[] toShortNameExpressions(Expression[] exprs) {
        if (exprs == null || exprs.length == 0) return exprs;
        Expression[] shortNameExpr = new Expression[exprs.length];
        for (int i = 0; i < exprs.length; i++) {
            if (exprs[i] instanceof PrimaryExpression) {
                List<String> t = ((PrimaryExpression) exprs[i]).getTuples();
                shortNameExpr[i] = new PrimaryExpression(Arrays.asList(t.get(t.size() - 1)));
            } else {
                shortNameExpr[i] = exprs[i];
            }
        }
        return shortNameExpr;
    }
   
    /**
     * Loads the class and member metadata for the given expression.
     *
     * @param expr the expression
     * @return the metadata for the given expression
     */
    private ExpressionMetaData getExpressionMetaData(Expression expr) {
        AbstractClassMetaData cmd = null;
        AbstractMemberMetaData mmd = null;
       
        ExpressionMetaData exprMetaData = null;
        if (expr instanceof PrimaryExpression) {
            exprMetaData = new ExpressionMetaData();
           
            List<String> ids = ((PrimaryExpression) expr).getTuples();
            String id = null;
            for (int i = 0; i < ids.size(); i++) {
                id = ids.get(i);
               
                // Load up the class meta data from the symbol (e.g. Select o.Id From User o
                // will load the metadata for User from the alias 'o')
                Symbol symbol = query.getCompilation().getSymbolTable().getSymbol(id);
                if (symbol != null) {
                    cmd = ec.getMetaDataManager().getMetaDataForClass(symbol.getValueType(), ec.getClassLoaderResolver());
                    exprMetaData.setClassMetaData(cmd);
                   
                // Get the member meta data from the class loaded above
                } else if (cmd != null) {
                    mmd = cmd.getMetaDataForMember(id);
                    if (mmd == null) {
                        throw new NucleusUserException("Symbol not found, entity: " + cmd.getName() + " symbol: " + id);
                    }
                   
                    exprMetaData.setMemberMetaData(mmd);
                } else {
                    throw new NucleusUserException("Could not find alias for field: " + id);
                }
            }
        }
        return exprMetaData;
    }
   
    private Object getDataForExpression(Map<String, ForceFetchFieldManager> ffms, SObject sObject, Expression expr,
            String fieldName, Object valueOverride) throws ConnectionException, SQLException {
        if (expr instanceof PrimaryExpression) {
            ExpressionMetaData exprMetaData = getExpressionMetaData(expr);
            if (exprMetaData != null) {
                AbstractMemberMetaData mmd = exprMetaData.getMemberMetaData();
                AbstractClassMetaData cmd = exprMetaData.getClassMetaData();
                if (cmd != null) {
                    ForceFetchFieldManager ffm = ffms.get(cmd.getName());
                    if (ffm == null) {
                        ffm =
                            new ForceFetchFieldManager(ec, cmd, (ForceStoreManager) ec.getStoreManager(), mconn, sObject, query);
                        ffms.put(cmd.getName(), ffm);
                    }
                    return ffm.fetchObjectField(mmd, valueOverride);
                }
            }
        } else if (expr instanceof InvokeExpression) {
            /**
             * COUNT always returns Long. Note count expressions never come here since SOQL returns 0 rows for that
             * MAX and MIN return the type of the expression they are applied on
             * AVG always returns Double (default)
             * SUM returns Long for an integral type, Double for a floating point types,
             *     BigInteger for BigInteger and BigDecimal for BigDecimal
             */
            InvokeExpression ev = (InvokeExpression) expr;
            String oper = ev.getOperation();
            if ("mapKey".equals(oper) || "mapValue".equals(oper) || "mapEntry".equals(oper)) {
                String alias = expr.getLeft().getSymbol().getQualifiedName();
                Map<String, Object> childrenMap =
                    (Map) getDataForExpression(ffms, sObject,
                            getPrimaryExpresionFromJoinAlias(query.getCompilation(), alias), fieldName, null);
                if ("mapKey".equals(oper)) {
                    return Collections.unmodifiableList(new ArrayList<String>(childrenMap.keySet()));
                } else if ("mapValue".equals(oper)) {
                    return Collections.unmodifiableList(new ArrayList<Object>(childrenMap.values()));
                } else {
                    return Collections.unmodifiableList(new ArrayList<Object>(childrenMap.entrySet()));
                }
            } else if ("MAX".equals(oper) || "MIN".equals(oper)) {
                /**
                 * We send a valueOverride which is the raw data and have it formatted according to the underlying
                 * field.
                 */
                Object value = sObject.getField(fieldName);
                if (value == null) return null;
                if (value instanceof Date || value instanceof Calendar) {
                    return value;
                }
                return getDataForExpression(ffms, sObject, ev.getArguments().get(0), fieldName, value.toString());
            } else if ("SUM".equals(oper)) {
                Object value = sObject.getField(fieldName);
                if (value == null) return value;
                /**
                 * SUM returns Long for an integral type, Double for a floating point types,
                 *     BigInteger for BigInteger and BigDecimal
                 */
                ExpressionMetaData exprMetaData = getExpressionMetaData(ev.getArguments().get(0));
                AbstractMemberMetaData mmd = exprMetaData.getMemberMetaData();
                if (mmd != null) {
                    if (mmd.getType() == Short.TYPE || mmd.getType() == Short.class
                            || mmd.getType() == Integer.TYPE || mmd.getType() == Integer.class
                            || mmd.getType() == Long.TYPE || mmd.getType() == Long.class) {
                        return ((Double) value).longValue();
                    } else if (mmd.getType() == BigInteger.class || mmd.getType() == BigDecimal.class) {
                        return getDataForExpression(ffms, sObject, ev.getArguments().get(0), fieldName, value.toString());
                    }
                }
            }
        }
        return sObject.getField(fieldName);
    }
   
    private PrimaryExpression getPrimaryExpresionFromJoinAlias(QueryCompilation compilation, String alias) {
        for (Expression fromExpr : compilation.getExprFrom()) {
            for (Expression expr = fromExpr.getRight(); expr != null; expr = expr.getRight()) {
                if (expr instanceof JoinExpression && alias.equals(expr.getAlias())) {
                    return ((JoinExpression) expr).getPrimaryExpression();
                }
            }
        }
        return null;
    }
   
    private List<Object> getResultAsCollection(SObject[] sObjects, List<String> fieldNameList) {
        List<Object> res = new ArrayList<Object>(sObjects.length);
        for (SObject sObject : sObjects) {
            List<Object> row = new ArrayList<Object>(fieldNameList.size());
            for (int i = 0; i < fieldNameList.size(); i++) {
                row.add(sObject.getField(fieldNameList.get(i)));
            }
            res.add(row.toArray());
        }
        return res;
    }
   
    private Collection<Object> readNonEntityObjects(SObject[] sObjects, Expression[] exprs, Class resultClass)
        throws ConnectionException, SQLException {
       
        // Create metadata first.
        // Assume the data comes back in the same order as the expressions and use first item to get the metadata
        List<String> fieldNameList = new ArrayList<String>();
        List<Expression> fieldNameExprs = new ArrayList<Expression>();
        boolean createFieldNameExpressions = resultClass != null;
        getFieldNameList(sObjects, createFieldNameExpressions, fieldNameList, fieldNameExprs);

        // Now read the data based on the metadata
        if (createFieldNameExpressions) {
            if (exprs != null && exprs.length > 0) {
                return readCreatorExpressionObjects(resultClass, sObjects, exprs, fieldNameList);
            } else {
                return new JDOQLResultClassMapper(resultClass).map(getResultAsCollection(sObjects, fieldNameList),
                                                                fieldNameExprs.toArray(new Expression[fieldNameExprs.size()]));
            }
        } else if (exprs != null && exprs.length > 0) {
            if (exprs[0] instanceof CreatorExpression) {
                CreatorExpression ce = (CreatorExpression) exprs[0];
                return readCreatorExpressionObjects(ce.getSymbol().getValueType(), sObjects,
                                ce.getArguments().toArray(new Expression[ce.getArguments().size()]), fieldNameList);
            } else {
                return readExpressionObjects(sObjects, exprs, fieldNameList);
            }
        } else {
            return getResultAsCollection(sObjects, fieldNameList);
        }
    }
   
    /**
     * This is used for single item fetch by ID.
     *
     *
     * @param table the table for the entity being queried
     * @param acmd  the class metadata for the entity being queried
     * @param fieldNumbers  the fields to fetch
     * @param pkValue the id of the object being fetched
     * @param fetchDepth the maximum depth that can be traversed
     *          by a query involving relationships
     * @return the SOQL query
     */
    public String buildQueryWithPK(TableImpl table, AbstractClassMetaData acmd, int[] fieldNumbers,
                                   String pkValue, int fetchDepth) {
        ExpressionBuilderHelper helper =
            new ExpressionBuilderHelper(this, fieldNumbers.length * 20 + 100, table, acmd,
                                            false, null, ec.getFetchPlan(), fetchDepth, null);
        helper.sb.append("select id");
        List<ColumnImpl> columns = new ArrayList<ColumnImpl>();
        for (int fieldNum : fieldNumbers) {
            columns.clear();
           
            List<ColumnImpl> cols =
                table.getColumnsFor(acmd, acmd.getMetaDataForManagedMemberAtAbsolutePosition(fieldNum),
                                        (ForceStoreManager) ec.getStoreManager(), columns);
            for (ColumnImpl col : cols) {
                if (!"id".equals(col.getForceApiName())) {
                    col.appendSelectString(helper, acmd, fieldNum, true, null);
                }
            }
        }
        helper.sb.append(" from ").append(table.getTableName().getForceApiName());
        helper.sb.append(String.format(" where %s='%s'", table.getPKFieldName(acmd), pkValue));
        String ret = helper.sb.toString();
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Fetch object: " + table.getTableName().getName() + " id: " + pkValue);
        }
        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("Fetch query: " + ret);
        }
        return helper.sb.toString();
    }
   
    /**
     *
     * Builds a query for fetching multiple top level objects.
     *
     * @param table  the table representing the object being queried
     * @param acmd  the class metadata for the object being queried
     * @param fieldsToLoad the set of fields to load
     * @param compilation the compile query
     * @param skipId  {@code true} if the id field should be skipped which might be necessary during joins
     * @param maxLimit the max number of entities that can be retrieved by this query
     * @param fetchPlan  the fetch plan used for this query
     * @param tableName  the name of the entity in Force.com
     * @return the SOQL query
     */
    public String buildQuery(TableImpl table, AbstractClassMetaData acmd, Set<Integer> fieldsToLoad, QueryCompilation compilation,
            boolean skipId, long maxLimit, FetchPlan fetchPlan, String tableName) {
        return buildQuery(table, acmd, fieldsToLoad, compilation, skipId, maxLimit, fetchPlan,
                            0, tableName, true, false, null, null);
    }
   
    /**
     *
     * Builds a query for fetching multiple objects, whether top level or part of a join query.
     *
     *
     * @param table  the table representing the object being queried
     * @param acmd  the class metadata for the object being queried
     * @param fieldsToLoad the set of fields to load
     * @param compilation the compile query
     * @param skipId  {@code true} if the id field should be skipped which might be necessary during joins
     * @param maxLimit the max number of entities that can be retrieved by this query
     * @param fetchPlan  the fetch plan used for this query
     * @param fetchDepth the depth that fetches should go (i.e. how many relationships can be traversed)
     * @param tableName  the name of the entity in Force.com
     * @param isTopLevel whether this object is the top level entity being queried or is part of a subquery
     * @param isJoin whether this is part of a join call
     * @param joinAlias the alias for a join query
     * @param parentHelper the expression builder for the parent query
     * @return the SOQL query
     */
    //CHECKSTYLE:OFF
    public String buildQuery(TableImpl table, AbstractClassMetaData acmd, Set<Integer> fieldsToLoad, QueryCompilation compilation,
                             boolean skipId, long maxLimit, FetchPlan fetchPlan, int fetchDepth,
                             String tableName, boolean isTopLevel, boolean isJoin, String joinAlias, ExpressionBuilderHelper parentHelper)  {
    //CHECKSTYLE:ON
        ExpressionBuilderHelper helper =
            new ExpressionBuilderHelper(this, (fieldsToLoad != null ? fieldsToLoad.size() : 3) * 20 + 100,
                table, acmd, isJoin, compilation, fetchPlan, fetchDepth, parentHelper);
        helper.sb.append("select ");
        if (compilation != null && compilation.getResultDistinct()) {
            throw new NucleusException("select distinct not supported by force.com datastore");
        }
        if (compilation != null && compilation.getExprResult() != null) {
            appendExpressionList(helper, compilation.getExprResult(), ec);
        } else {
            int count = 0;
            if (!skipId) {
                helper.sb.append("id");
                count++;
            }
            if (fieldsToLoad != null) {
                /**
                 * If there are relatedObject filters eager load them if not already loaded
                 */
                if (helper.relatedJoinAliases != null) {
                    for (Map.Entry<TupleName, String> ent : helper.relatedJoinAliases.entrySet()) {
                        TupleName fullName = ent.getKey();
                        int pos = acmd.getAbsolutePositionOfMember(fullName.getShortName());
                        if (pos < 0) {
                            throw new NucleusDataStoreException("Cannot locate member metadata for field: "
                                                                + fullName.getLongName());
                        }
                        fieldsToLoad.add(pos);
                    }
                }

                List<ColumnImpl> columns = new ArrayList<ColumnImpl>();
                for (int fieldNum : fieldsToLoad) {
                    columns.clear();

                    List<ColumnImpl> cols =
                        table.getColumnsFor(acmd, acmd.getMetaDataForManagedMemberAtAbsolutePosition(fieldNum),
                                                (ForceStoreManager) ec.getStoreManager(), columns);
                    for (ColumnImpl col : cols) {
                        if (!"id".equals(col.getForceApiName())) {
                            if (col.appendSelectString(helper, acmd, fieldNum, count > 0, null)) {
                                count++;
                            }
                        }
                    }
                }
            }
        }
        helper.sb.append(" from ").append(tableName);
        if (compilation != null) {
            String alias = isTopLevel ? compilation.getCandidateAlias() : joinAlias;
            if (alias != null) {
                helper.sb.append(String.format(" %s ", alias));
            }
           
            // Do the where clause
            Expression filterExpression = helper.getFilterExpression(alias);
            if (filterExpression != null) {
                helper.sb.append(" where (");
                appendExpression(helper, filterExpression, ec);
                helper.sb.append(")");
            }
           
            // Do any join processing for top level but not for Map/Collection joins
            if (isTopLevel && helper.relatedJoinAliases == null) {
                processJoin(helper, compilation, fetchPlan, filterExpression != null);
            }

            // Do the group by clause
            if (isTopLevel && compilation.getExprGrouping() != null) {
                helper.sb.append(" group by ");
                appendExpressionList(helper, compilation.getExprGrouping(), ec);
            }
            // Do the having clause
            if (isTopLevel && compilation.getExprHaving() != null) {
                if (compilation.getExprGrouping() == null) {
                    throw new NucleusException("Queries specifying a HAVING clause must also specify a GROUP BY clause");
                }
                Expression expr = compilation.getExprHaving();
                boolean isAggregate = isAggregate(expr);
                if (!isAggregate) {
                    throw new NucleusException("HAVING clauses must reference an aggregate function");
                }
                helper.sb.append(" having ");
                appendExpression(helper, compilation.getExprHaving(), ec);
            }
            // Do the order by clause
            if (isTopLevel && compilation.getExprOrdering() != null) {
                helper.sb.append(" order by ");
                appendExpressionList(helper, compilation.getExprOrdering(), ec);
            }
        }
        // Add limit if needed
        if (maxLimit > 0 && maxLimit < Long.MAX_VALUE) {
            helper.sb.append(" limit ");
            helper.sb.append(maxLimit);
        }
        String ret = helper.sb.toString();
        if (listeners != null && !listeners.isEmpty()) {
            for (QueryListener listener : listeners.values()) {
                listener.listen(ret);
            }
        }
        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("Table: " + table.getTableName().getName() + " query: " + ret);
        }
        return ret;
    }
   
    private void processJoin(ExpressionBuilderHelper h, QueryCompilation compilation, FetchPlan fetchPlan, boolean hasWhere) {
        if (compilation.getExprFrom() == null) return;
        for (Expression fromExpr : compilation.getExprFrom()) {
            if (!(fromExpr instanceof ClassExpression)) {
                throw new NucleusDataStoreException("Unsupported from expression: " + fromExpr);
            }
            Expression expr = fromExpr.getRight();
            while (expr != null) {
                if (expr instanceof JoinExpression) {
                    JoinExpression je = (JoinExpression) expr;
                    processJoinExpression(je.getPrimaryExpression(), je.getAlias(), h, compilation, true, hasWhere, false);
                    h.sb.append("))");
                    // Either this was already true or will be after the first call
                    hasWhere = true;
                } else {
                    throw new NucleusDataStoreException("Unsupported expression found inside a join expression: "
                                                        + fromExpr.getRight());
                }
                expr = expr.getRight();
            }
        }
    }
   
    /**
     * Handles explicit JOIN o.ownerId type of joins or implicit member of o.children.name.
     *
     * @return h.sb is updated with in where (select ...) query.
     *         The query is unclosed and the caller is expected to close it with ')'
     */
    private void processJoinExpression(PrimaryExpression pe, String alias, ExpressionBuilderHelper h,
            QueryCompilation compilation, boolean needsWhere, boolean hasWhere, boolean not)  {
        AbstractClassMetaData cmd = h.acmd;
        AbstractMemberMetaData mmd = null;
        List<String> ids = pe.getTuples();
        String id = null;
        for (int i = 0; i < ids.size(); i++) {
            id = ids.get(i);
            Symbol symbol = compilation.getSymbolTable().getSymbol(id);
            if (symbol != null) {
                cmd = ec.getMetaDataManager().getMetaDataForClass(symbol.getValueType(), ec.getClassLoaderResolver());
            } else {
                mmd = cmd.getMetaDataForMember(id);
                if (mmd == null) {
                    throw new NucleusUserException("Symbol not found, entity: " + cmd.getName() + " symbol: " + id);
                }
                // If these are collections get underlying object metadata
                if (mmd.getCollection() != null || mmd.getMap() != null) {
                    cmd = PersistenceUtils.getMemberElementClassMetaData(mmd, ec.getClassLoaderResolver(),
                                                                            ec.getMetaDataManager());
                } else {
                    cmd = ec.getMetaDataManager().getMetaDataForClass(mmd.getType(), ec.getClassLoaderResolver());
                }
            }
        }

        TableImpl joinTable = ((ForceStoreManager) ec.getStoreManager()).getTable(cmd);
        Set<Integer> joinFieldsToLoad = null;
        String inField = null;
        boolean skipId = true;
        if (mmd.getMappedBy() != null) {
            //Now find the ammd that is the FK to this ammd since the relationship can be invoked from that direction
            mmd = cmd.getMetaDataForMember(mmd.getMappedBy());
            joinFieldsToLoad = Collections.singleton(mmd.getAbsoluteFieldNumber());
            inField = "id";
        } else {
            // Here we select id of the Entity but the inField has to be the FK name
            joinFieldsToLoad = Collections.singleton(cmd.getPKMemberPositions()[0]);
            TableImpl joiningTable = ((ForceStoreManager) ec.getStoreManager()).getTable(mmd.getAbstractClassMetaData());
            inField = joiningTable.getColumnByJavaName(mmd.getName()).getFieldName();
            skipId = false;
        }
        if (needsWhere) {
            if (hasWhere) {
                h.sb.append(" and (");
            } else {
                h.sb.append(" where (");
            }
        }
        h.sb.append(inField);
        if (not) h.sb.append(" not");
        h.sb.append(" in (").append(buildQuery(joinTable, cmd, joinFieldsToLoad, compilation, skipId,
                                                0, h.fetchPlan, h.fetchDepth, joinTable.getTableName().getForceApiName(),
                                                false, true, alias, h));
    }
   
    private boolean isAggregate(Expression expr) {
        if (expr == null) return false;
       
        if (expr instanceof InvokeExpression) {
            String methodName = ((InvokeExpression) expr).getOperation();
            return AGGREGATE_METHODS.contains(methodName);
        } else {
            return isAggregate(expr.getRight()) || isAggregate(expr.getLeft());
        }
    }
   
    private void appendExpressionList(ExpressionBuilderHelper h, Expression[] exprList, ExecutionContext executionContext)  {
        for (int i = 0; i < exprList.length; i++) {
            if (i > 0) {
                h.sb.append(", ");
            }
            appendExpression(h, exprList[i], executionContext);
        }
    }

    private boolean appendExpression(ExpressionBuilderHelper h, Expression expr, ExecutionContext executionContext)  {
        if (expr == null) return false;
        boolean done = false;
        if (h.level > MAX_EXPRESSION_DEPTH) {
            throw new NucleusException(
                    "Expression too deep. Max depth reached at: " + expr);
        }
        h.level++;

        try {
            if (expr instanceof DyadicExpression) {
                if (expr.getOperator() instanceof MonadicOperator) {
                    if (expr.getLeft() instanceof InvokeExpression) {
                        appendInvokeExpression(h, (InvokeExpression) expr.getLeft(),
                                                executionContext, expr.getOperator() == Expression.OP_NOT);
                    } else {
                        h.sb.append(expr.getOperator().toString());
                        appendExpression(h, expr.getLeft(), executionContext);
                    }
                } else {
                    boolean lparens = expr.getLeft() instanceof DyadicExpression;
                    if (lparens) h.sb.append("( ");
                    boolean skip = appendExpression(h, expr.getLeft(), executionContext);
                    if (lparens) h.sb.append(" )");

                    /**
                     * There are certain InvokeExpressions that are rewritten to not require the right expression.
                     * However, we do not want to propagate doneness any further
                     */
                    if (!skip) {
                        h.sb.append(expr.getOperator().toString());
   
                        boolean rparens = expr.getRight() instanceof DyadicExpression;
                        if (rparens) h.sb.append("( ");
                        appendExpression(h, expr.getRight(), executionContext);
                        if (rparens) h.sb.append(" )");
                    }
                }
            } else if (expr instanceof PrimaryExpression) {
                // Recurse!
                appendExpression(h, expr.getLeft(), executionContext);
                appendExpression(h, expr.getRight(), executionContext);
                List<String> ids = ((PrimaryExpression) expr).getTuples();
                int pos = 0;
                TableImpl t = h.table;
                AbstractClassMetaData cmd = h.acmd;
                for (String id : ids) {
                    if (pos++ > 0) {
                        h.sb.append(".");
                    }
                    String name = id;
                    if (pos == ids.size()) {
                        ColumnImpl column = t.getColumnByJavaName(id);
                        if (column != null) {
                            name = column.getFieldName();
                        }
                    } else {
                        AbstractMemberMetaData mmd = cmd.getMetaDataForMember(id);
                        if (mmd != null) {
                            ColumnImpl column = t.getColumnByJavaName(mmd.getName());
                            name = column.getForceApiRelationshipName();
                            AbstractClassMetaData tcmd =
                                executionContext.getMetaDataManager().getMetaDataForClass(mmd.getType(), null);
                            if (tcmd != null) {
                                cmd = tcmd;
                                t = ((ForceStoreManager) executionContext.getStoreManager()).getTable(cmd);
                            }
                        }
                    }
                    h.sb.append(name);
                }
            } else if (expr instanceof InvokeExpression) {
                done = appendInvokeExpression(h, (InvokeExpression) expr, executionContext, false);
            } else if (expr instanceof ParameterExpression) {
                appendValue(h, getParameterValue(h, (ParameterExpression) expr, executionContext));
            } else if (expr instanceof VariableExpression) {
                VariableExpression varExpr = (VariableExpression) expr;
                if (varExpr.getSymbol() != null && varExpr.getSymbol().getQualifiedName() != null) {
                    /**
                     * This is vanilla subquery processing, i.e. select ... (select ...) created from JPQL/JDOQL
                     */
                    QueryCompilation subCompilation =
                        h.compilation.getCompilationForSubquery(varExpr.getSymbol().getQualifiedName());
                    if (subCompilation != null) {
                        AbstractClassMetaData cmd =
                            executionContext.getMetaDataManager().getMetaDataForClass(subCompilation.getCandidateClass(),
                                                                            executionContext.getClassLoaderResolver());
                        TableImpl joinTable = ((ForceStoreManager) executionContext.getStoreManager()).getTable(cmd);
                        // Pretend it's a top level query since it has its own compilation. Also fieldsList is null as
                        // we want select to use subCompilation.getExprResult()
                        h.sb.append("(").append(buildQuery(joinTable, cmd, null, subCompilation, true,
                                                            0, h.fetchPlan, joinTable.getTableName().getForceApiName()))
                                        .append(")");
                    }
                } else {
                    throw new NucleusUserException(
                            "Unexpected expression type while parsing query.  Are you certain that a field named "
                                    + varExpr.getId() + " exists on your object?");
                }
            } else if (expr instanceof Literal) {
                Object literal = ((Literal) expr).getLiteral();
                appendValue(h, literal);
            } else if (expr instanceof OrderExpression) {
                appendExpression(h, expr.getLeft(), executionContext);
                OrderExpression order = (OrderExpression) expr;
                h.sb.append(String.format(" %s ", "ascending".equals(order.getSortOrder()) ? "ASC" : "DESC"));
            } else if (expr instanceof CreatorExpression) {
                CreatorExpression ce = (CreatorExpression) expr;
                appendExpressionList(h, ce.getArguments().<Expression>toArray(new Expression[ce.getArguments().size()]),
                                        executionContext);
            } else if (expr instanceof SubqueryExpression) {
                SubqueryExpression subExpr = (SubqueryExpression) expr;
                if ("EXISTS".equals(subExpr.getKeyword())) {
                    throw new NucleusUserException("EXISTS is not supported in force.com database");
                } else if (UNSUPPORTED_JOINS.contains(subExpr.getKeyword())) {
                    throw new NucleusUserException(subExpr.getKeyword() + " is not supported in force.com database");
                } else {
                    throw new NucleusUserException("Unexpected subquery expression: " + subExpr);
                }
            } else {
                throw new NucleusException(
                        "Unexpected expression type while parsing query: " + expr.getClass().getName());
            }
        } finally {
            h.level--;
        }
        return done;
    }
   
    private TupleName getMappedExpression(ExpressionBuilderHelper h, InvokeExpression expr) {
        String alias = expr.getLeft().getSymbol().getQualifiedName();
        TupleName fieldName = null;
        for (Map.Entry<TupleName, String> ent : h.relatedJoinAliases.entrySet()) {
            if (ent.getValue().equals(alias)) {
                fieldName = ent.getKey();
                break;
            }
        }
        if (fieldName == null) {
            throw new NucleusDataStoreException("Cannot find field name for alias: " + alias);
        }
        return fieldName;
    }
   
    private void appendMappedByExpression(ExpressionBuilderHelper h, InvokeExpression expr) {
        TupleName fieldName = getMappedExpression(h, expr);
        /**
         * The key is defined by @MapKey on the Map field and defaulted to "id"
         */
        String columnToAdd = "id";
        AbstractClassMetaData acmd = fieldName.getTuple().size() > 1
                ? ec.getMetaDataManager()
                        .getMetaDataForClass(h.compilation.getSymbolTable().getSymbol(fieldName.getShortNamePrefix())
                                .getValueType(), ec.getClassLoaderResolver()) : h.acmd;
        AbstractMemberMetaData ammd = acmd.getMetaDataForMember(fieldName.getShortName());
        if (ammd.getKeyMetaData() != null) {
            AbstractClassMetaData cmd =
                PersistenceUtils.getMemberElementClassMetaData(ammd, ec.getClassLoaderResolver(), ec.getMetaDataManager());
            if (cmd != null) {
               TableImpl joinedTable = ((ForceStoreManager) ec.getStoreManager()).getTable(cmd);
                ColumnImpl col = joinedTable.getColumnByJavaName(ammd.getKeyMetaData().getMappedBy());
                if (col != null) {
                    columnToAdd = col.getFieldName();
                }
            }
        }
        h.sb.append(columnToAdd);
    }

    private boolean appendInvokeExpression(ExpressionBuilderHelper h, InvokeExpression invocation,
                                            ExecutionContext executionContext, boolean not) {
        boolean done = false;
        String oper = invocation.getOperation();
        if ("matches".equals(oper)) {
            if (not) h.sb.append("NOT ");
            appendExpression(h, invocation.getLeft(), executionContext);
            h.sb.append(" like ");
            appendExpression(h, invocation.getArguments().get(0), executionContext);
        } else if ("toLowerCase".equals(oper)) {
            //ignore toLowerCase since SFDC is case insensitive
            appendExpression(h, invocation.getLeft(), executionContext);
        } else if ("COUNT".equals(oper)) {
            h.sb.append(String.format(" %s()", oper));
        } else if ("CURRENT_DATE".equals(oper)) {
            h.sb.append(String.format(" %s", getCurrentDateHint(h)));
        } else if ("CURRENT_TIMESTAMP".equals(oper) || "CURRENT_TIME".equals(oper)) {
            throw new NucleusUserException("CURRENT_TIMESTAMP or CURRENT_TIME is not supported by Force.com datastore");
        } else if ("mapKey".equals(oper) || "mapValue".equals(oper) || "mapEntry".equals(oper)) {
            if (h.isInSelect) {
                /**
                 * Based JSR-317 Final Release section 4.4.4 Path Expressions I interpret valueOf(p) = p
                 */
                TupleName fieldName = getMappedExpression(h, invocation);
                ColumnImpl column = h.table.getColumnByJavaName(fieldName.getShortName());
                if (column != null) {
                    column.appendSelectString(h, h.acmd,
                                                h.acmd.getAbsolutePositionOfMember(fieldName.getShortName()), false, null);
                }
            } else {
                appendMappedByExpression(h, invocation);
            }
        } else if ("contains".equals(oper)) {
            appendContainsExpression(h, invocation, not);
        } else if ("size".equals(oper)) {
            boolean isEmpty = invocation.getParent().getOperator() == Expression.OP_EQ;
            // If the method isEmpty we request !contains; if isnotEmpty we request contains
            appendContainsExpression(h, invocation, isEmpty);
            // Now there is leftover = 0 expression that needs to be skipped
            done = true;
        } else {
            // The format here is operation(left, arguments ...);
            if (not) h.sb.append(" NOT ");
            h.sb.append(String.format(" %s(", oper));
            appendExpression(h, invocation.getLeft(), executionContext);

            int pos = 0;
            for (Expression e : invocation.getArguments()) {
                if (pos++ > 0) {
                    h.sb.append(", ");
                }
                appendExpression(h, e, executionContext);
            }
            h.sb.append(") ");
            appendExpression(h, invocation.getRight(), executionContext);
        }
        return done;
    }
   
    private void appendContainsExpression(ExpressionBuilderHelper h, InvokeExpression expr, boolean not)  {
        PrimaryExpression pe = (PrimaryExpression) expr.getLeft();
        TupleName fieldName = new TupleName(pe.getTuples());
        AbstractMemberMetaData ammd = h.acmd.getMetaDataForMember(fieldName.getShortName());
        if (ammd != null && (ammd.getMap() != null || ammd.getCollection() != null)) {
            processJoinExpression(pe, null, h, h.compilation, false, false, not);
            if (expr.getArguments().size() > 0) {
                String name = hints != null ? (String) hints.get(QueryHints.MEMBER_OF_FIELD) : null;
                h.sb.append(" where ");
                appendExpression(h, new PrimaryExpression(new TupleName(name != null ? name : "name").getTuple()), ec);
                h.sb.append(" = ");
                appendExpression(h, expr.getArguments().get(0), ec);
            }
            h.sb.append(")");
        } else {
            // This is simply picklist values
            appendExpression(h, expr.getLeft(), ec);
            if (expr.getArguments().size() > 0) {
                h.sb.append(not ? " excludes(" : " includes(");
                Literal l = (Literal) expr.getArguments().get(0);
                h.sb.append(new MultiPicklistFormatter(l.getLiteral().toString()).getFormattedString());
                h.sb.append(")");
            } else {
                h.sb.append(not ? " = " : " != ").append("null");
            }
        }
    }
   
    private String getCurrentDateHint(ExpressionBuilderHelper h) {
        Object hint = hints != null ? hints.get(QueryHints.CURRENT_DATE) : null;
        if (hint instanceof String[]) {
            String[] hArray = (String[]) hint;
            if (currentHint < hArray.length) {
                hint = hArray[currentHint++];
            }
        }
        if (hint instanceof String) {
            return (String) hint;
        }
        return "TODAY";
    }
   
    private void appendValue(ExpressionBuilderHelper h, Object value) {
      Class clazz = null;
      if(value != null) {
        clazz = value.getClass();
      }
        if (clazz == String.class || clazz == Character.class || clazz == URL.class
                || clazz == Byte.class || clazz == byte.class) {
            h.sb.append(String.format("'%s'", value));
        } else if (clazz == Date.class || value instanceof Calendar) {
            if (clazz == Date.class) {
                h.sb.append(SOQLDateFormatUtil.getSOQLFormat((Date) value));
            } else {
                h.sb.append(SOQLDateFormatUtil.getSOQLFormat((Calendar) value));
            }
        } else {
            h.sb.append(value);
        }
    }
   
    private Object getParameterValue(ExpressionBuilderHelper h, ParameterExpression expr, ExecutionContext executionContext) {
        Object paramValue = parameters.get(expr.getId());
        if (paramValue != null) return getTransformedValueFromParamValue(paramValue, executionContext);

        // Check if a positional param is set
        try {
            paramValue = parameters.get(Integer.parseInt(expr.getId()));
            if (paramValue != null) {
                return getTransformedValueFromParamValue(paramValue, executionContext);
            }
            return paramValue;
        } catch (NumberFormatException e) {
            // we are going to settle for null here
        }
        return new NucleusException("Cannot find parameter expression: " + expr.toString());
    }
   
    private Object getTransformedValueFromParamValue(Object paramValue, ExecutionContext executionContext) {
        AbstractClassMetaData acmd =
            executionContext.getMetaDataManager().getMetaDataForClass(paramValue.getClass(),
                                                                        executionContext.getClassLoaderResolver());
        if (acmd != null && paramValue instanceof PersistenceCapable) {
            return getIdFromObject((PersistenceCapable) paramValue, acmd).toString();
        } else if (paramValue instanceof Collection<?>) {
            return new CollectionParameter((Collection<?>) paramValue);
        } else {
            return paramValue;
        }
    }
   
    /**
     * Convenience method for getting the id value from an entity.
     *
     * @param entity the entity containing the id
     * @param acmd the class metadata (for discovering the id field)
     * @return the id value of the object
     */
    public static Object getIdFromObject(PersistenceCapable entity, AbstractClassMetaData acmd) {
        Object ret = null;
        AbstractMemberMetaData ammd = acmd.getMetaDataForManagedMemberAtAbsolutePosition(acmd.getPKMemberPositions()[0]);
        try {
            ret = PersistenceUtils.getMemberValue(acmd, acmd.getPKMemberPositions()[0], entity);
            if (ret instanceof String) {
                ret = new StringIdentity(ammd.getType(), (String) ret);
            }
            return ret;
        } catch (Exception e) {
            throw new NucleusDataStoreException(e.getMessage(), e);
        }
    }
   
    /**
     * Helper method to handle joining a relationship field to a query that is currently being built.
     *
     * @param helper  the expression builder with the in progress query
     * @param ammd  the member metadata for the relationship being appended
     * @param col  the column data for the relationship field
     */
    public void appendRelationshipQuery(ExpressionBuilderHelper helper, AbstractMemberMetaData ammd, ColumnImpl col)  {
        FetchPlan fetchPlan = ec.getFetchPlan();
        Set<Integer> joinFieldsToLoad = getFieldsToLoad(helper.acmd, fetchPlan);
        String relName = col.getForceApiRelationshipName();
        helper.getBuilder().append("(")
                           .append(buildQuery(helper.table, helper.acmd, joinFieldsToLoad, null, false,
                                               0, fetchPlan, helper.fetchDepth, relName, false, false, null, null
                           ));
        /**
         * If there is a filter for these related object in the context use it
         * Else use any JoinFilters
         * Else no filter
         */
       TupleName name = new TupleName(ammd.getName());
       if (helper.relatedJoinAliases != null && helper.relatedJoinAliases.containsKey(name)) {
            Expression filter = helper.aliasToFilterMappings.get(helper.relatedJoinAliases.get(name));
            if (filter == null) {
                throw new NucleusDataStoreException("Could not locate related filter for alias: "
                                                    + helper.relatedJoinAliases.get(name));
            }
            helper.getBuilder().append(" ").append(helper.relatedJoinAliases.get(name)).append(" where (");
            boolean oldIsInSelect = helper.isInSelect;
            helper.isInSelect = false;
            appendExpression(helper, filter, ec);
            helper.isInSelect = oldIsInSelect;
            helper.getBuilder().append(")");
        } else {
            JoinFilter joinFilter = PersistenceUtils.getMemberAnnotation(ammd.getMemberRepresented(), JoinFilter.class);
            if (joinFilter != null) {
                if (joinFilter.alias().length() > 0) {
                    helper.getBuilder().append(String.format(" %s", joinFilter.alias()));
                }
                if (joinFilter.value().length() > 0) {
                    helper.getBuilder().append(" where (").append(joinFilter.value()).append(")");
                }
            }
        }
        if (ammd.getOrderMetaData() != null && ammd.getOrderMetaData().getOrdering() != null
                && !ammd.getOrderMetaData().getOrdering().equals("#PK")) {
            JPQLPartialCompiler partialCompiler = new JPQLPartialCompiler(ec, helper.compilation, null, parameters);
            helper.getBuilder().append(" order by ");
            String orderBy = ammd.getOrderMetaData().getOrdering();
            if (orderBy == null || orderBy.length() == 0) {
                orderBy = "id";
            }
            Expression[] exprList = partialCompiler.compileOrdering(orderBy);
            appendExpressionList(helper, exprList, ec);
        }
        helper.getBuilder().append(")");
    }
   
    /**
     * Append the fields in the default fetch group for the entity in the relationship to the query.
     *
     * @param helper  the expression builder with the in progress query
     * @param col  the column data for the relationship field
     * @param prefix  the prefix to add to the relationship field
     */
    public void appendRelationshipFields(ExpressionBuilderHelper helper, ColumnImpl col, String prefix) {
        // Add all the fields for this table
        List<ColumnImpl> columns = new ArrayList<ColumnImpl>();
        int count = 0;
        String newPrefix = prefix != null ? prefix + col.getForceApiRelationshipName() + "."
                                                : col.getForceApiRelationshipName() + ".";
        for (int num : helper.acmd.getDFGMemberPositions()) {
            columns.clear();
           
            List<ColumnImpl> cols =
                helper.table.getColumnsFor(helper.acmd, helper.acmd.getMetaDataForManagedMemberAtAbsolutePosition(num),
                                            (ForceStoreManager) ec.getStoreManager(), columns);
            for (ColumnImpl c : cols) {
                if (c.appendSelectString(helper, helper.acmd, num, count > 0, newPrefix)) {
                    count++;
                }
            }
        }
    }
   
    /**
     * Simple bean which stores JPA class and member metadata
     * for a DataNucleus Expression.
     *
     * @author Tim Kral
     */
    private static final class ExpressionMetaData {
        private AbstractClassMetaData acmd;
        private AbstractMemberMetaData ammd;
       
        private ExpressionMetaData() {  }
       
        private AbstractClassMetaData getClassMetaData() {
            return this.acmd;
        }
       
        private void setClassMetaData(AbstractClassMetaData classMetaData) {
            this.acmd = classMetaData;
        }
       
        private AbstractMemberMetaData getMemberMetaData() {
            return this.ammd;
        }
       
        private void setMemberMetaData(AbstractMemberMetaData memberMetaData) {
            this.ammd = memberMetaData;
        }
    }
}
TOP

Related Classes of com.force.sdk.jpa.query.ForceQueryUtils$ExpressionMetaData

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.