/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 * 
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License. You can obtain
 * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 * 
 * Contributor(s):
 * 
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

/*
 * SelectStatement.java
 *
 * Created on October 3, 2001
 *
 */

package com.sun.jdo.spi.persistence.support.sqlstore.sql.generator;

import org.netbeans.modules.dbschema.ColumnElement;

import com.sun.jdo.api.persistence.support.JDOFatalDataStoreException;
import com.sun.jdo.api.persistence.support.JDOFatalInternalException;
import com.sun.jdo.spi.persistence.support.sqlstore.ActionDesc;
import com.sun.jdo.spi.persistence.support.sqlstore.ValueFetcher;
import com.sun.jdo.spi.persistence.support.sqlstore.database.DBVendorType;
import com.sun.jdo.spi.persistence.support.sqlstore.model.TableDesc;
import com.sun.jdo.spi.persistence.support.sqlstore.sql.RetrieveDescImpl;
import com.sun.jdo.spi.persistence.support.sqlstore.sql.constraint.ConstraintField;
import com.sun.jdo.spi.persistence.support.sqlstore.sql.constraint.ConstraintFieldDesc;
import com.sun.jdo.spi.persistence.support.sqlstore.sql.constraint.ConstraintNode;
import com.sun.jdo.spi.persistence.support.sqlstore.sql.constraint.ConstraintOperation;
import com.sun.jdo.spi.persistence.support.sqlstore.sql.constraint.ConstraintJoin;
import com.sun.jdo.spi.persistence.utility.I18NHelper;

import java.util.ArrayList;
import java.util.List;
import java.sql.SQLException;


/**
 * This class generates select statements.
 */
public class SelectStatement extends Statement {

    /** Flag indicating if this statement has been joined. */
    private boolean isJoined;

    private StringBuffer orderClause = new StringBuffer();

    /** SelectQueryplan */
    SelectQueryPlan plan;

    public SelectStatement(DBVendorType vendorType, SelectQueryPlan plan) {
        super(vendorType);
        this.plan = plan;
        constraint = plan.getConstraint();
    }

    public boolean isJoined() {
        return isJoined;
    }

    public void markJoined() {
        isJoined = true;
    }

    public ColumnRef addColumn(ColumnElement columnElement,
                          QueryTable queryTable) {

        ColumnRef columnRef = null;

        if ((columnRef = getColumnRef(columnElement)) == null) {
            columnRef = new ColumnRef(columnElement, queryTable);
            addColumnRef(columnRef);
        }

        return columnRef;
    }

    public void copyColumns(SelectStatement sourceStatement) {
        ArrayList columnRefs = sourceStatement.getColumnRefs();

        int index = columns.size() + 1;

        for (int i = 0; i < columnRefs.size(); i++) {
            //addColumnRef((ColumnRef) columnRefs.get(i));
            ColumnRef cref = (ColumnRef) columnRefs.get(i);
            cref.setIndex(index + i);
            columns.add(cref);
        }
    }

    protected boolean isUpdateLockRequired(QueryTable table) {
        return (plan.options & RetrieveDescImpl.OPT_AGGREGATE) == 0
                && table.getTableDesc().isUpdateLockRequired();
    }

    public void appendTableText(StringBuffer text, QueryTable table) {
        super.appendTableText(text, table);

        if (isUpdateLockRequired(table)) {
            //Append eqivalent of "with (updlock)" to the table text
            text.append(vendorType.getHoldlock() );

            //For efficiency, the test whether a database is capable of supporting
            //holdlock would be made in method getText() where we generate
            //text corresponding to vendorType.getForUpdate()
        }
    }

    /**
      * Determines if Column Type definition is needed for this statement.
      * Column Type definition is a performance optimization that allows defining
      * Column Type for the resultset.
      * If the query to be executed is counting pc instances, the column used
      * inside COUNT() is one of the pk columns. If the pk column happens to be
      * not convertable to an int (for example timestamp), database will throw an
      * exception. To prevent this situation, column type definition should not
      * be performed on such queries.
      *
      * @return true if column type definition is needed, false otherwise.
      */
     public boolean isColumnTypeDefinitionNeeded() {
         return (plan.options & RetrieveDescImpl.OPT_COUNT_PC ) == 0;
     }

    /** @inheritDoc */
    public QueryPlan getQueryPlan() {
        return plan;
    }

    /**
     * @inheritDoc
     */
    protected void generateStatementText() {
        // Because join conditions for ANSI outer joins end up in the
        // from clause, the constraint stack has to be processed before we
        // generate the from clause.
        StringBuffer constraints = processConstraints();
        StringBuffer outerJoinText = processOuterJoinConstraints();

        if (outerJoinText != null && outerJoinText.length() > 0) {
            if (constraints.length() > 0 ) {
                constraints.append(" and ");
            }
            constraints.append(outerJoinText);
        }

        StringBuffer whereClause = new StringBuffer();

        if (constraints.length() > 0 ) {
            whereClause.append(" where ").append(constraints);
        }

        if ((plan.options & RetrieveDescImpl.OPT_COUNT_PC) == 0) {
            generateRegularStatementText(whereClause);
        } else {
            generateCountStatementText(whereClause);
        }
    }

    /**
     * Generates the statement text for a "regular" select query.
     * Count queries have to get special attention in case of objects
     * with composite primary key.
     *
     * @param whereClause Query's where clause.
     * @see #generateCountStatementText
     */
    private void generateRegularStatementText(StringBuffer whereClause) {

        statementText = new StringBuffer();

        StringBuffer columnText = generateColumnText();
        String tableListText = generateTableListText();
        String aggregateText = getAggregateText();
        String aggregateEnd = (aggregateText.length() > 0) ? ")" : ""; // NOI18N

        if (orderClause.length() > 0) {
            orderClause.insert(0, " order by ");
        }

        final boolean updateLockRequired = isUpdateLockRequired();
        StringBuffer forUpdateClause = generateForUpdateClause(updateLockRequired);
        String distinctText = getDistinctText(updateLockRequired);

        // Create the query filling in the column list, table name, etc.
        statementText.append("select "). // NOI18N
                append(aggregateText).append(distinctText).append(columnText).append(aggregateEnd).
                append(" from ").append(tableListText). // NOI18N
                append(whereClause).append(orderClause).append(forUpdateClause);
    }

    /**
     * Generates the statement text for a count query. Count queries
     * on persistence capable objects have been mapped to selecting the
     * primary key columns in
     * {@link SelectQueryPlan#addFetchGroups(int, ArrayList, ArrayList)}.
     * Queries w/o a distinct restriction can be relaxed to select only
     * one of the primary key columns. Distinct queries on objects with
     * composite primary key have to be treated special to avoid duplicates.
     *
     * @param whereClause Query's where clause.
     * @see #generateCorrelatedExistsText
     */
    private void generateCountStatementText(StringBuffer whereClause) {

        final int selectedColumns = columns.size();

        if (selectedColumns == 1) {
            // Single PK. Call regular statement generation.
            generateRegularStatementText(whereClause);
        } else {
            boolean oneTable = tableList.size() == 1;

            if ((plan.options & RetrieveDescImpl.OPT_DISTINCT) == 0
                    || oneTable) {
                // Without DISTINCT or when querying just one table, we can
                // select only one of the pk columns and get the correct result.
                // Remove the rest.
                for (int i = selectedColumns; i > 1; ) { columns.remove(--i); }
                if (oneTable) {
                    // When selecting only one table, remove the DISTINCT
                    // contraint from the query options to get all rows.
                    plan.options &= ~RetrieveDescImpl.OPT_DISTINCT;
                }
                // Now call regular statement generation.
               generateRegularStatementText(whereClause);
            } else {
                // This is a distinct count on objects having a composite pk and the query
                // includes join constraints. We map this to a correlated exists query.
                // Note: columns can only be cleared after this call.
                generateCorrelatedExistsText(whereClause);

                // Since we're not selecting any columns in this query, remove all column
                // information. Oracle's special DB operation code gets confused otherwise.
                // See OracleSpecialDBOperation#defineColumnTypeForResult(Statement, List)
                columns.clear();
            }
        }
    }

    /**
     * Generates the text for a correlated exists query. Count distinct queries on
     * objects with composite primary key are mapped to a correlated exists query.
     * The "outer" select on the primary table to is correlated
     * to the "inner" select by the already generated where clause.
     *
     * @param whereClause Query's where clause.
     */
    private void generateCorrelatedExistsText(StringBuffer whereClause) {

        statementText = new StringBuffer();

        // TODO: Use correlated exists subquery in SelectQueryPlan?
        // - Do we handle secondary tables correctly?
        // - Do we need order by and for update clauses?

        // Generate for update clause while we still have all tables in tableList
        boolean updateLockRequired = isUpdateLockRequired();
        StringBuffer forUpdateClause = generateForUpdateClause(updateLockRequired);

        StringBuffer primaryTableText = new StringBuffer();
        QueryTable primaryTable = generatePrimaryTableText(primaryTableText);

        // Prepare the generation of the correlated "inner" select clause by
        // removing the primary table from the table list.
        // As count queries are never generated internally, we're preparing
        // a user query here. User queries never have outer joins. It's safe
        // remove the table from tableList.
        tableList.remove(primaryTable);
        String tableListText = generateTableListText();

        // Create the query with the previous generated parts.
        statementText.append("select count(*) from "). // NOI18N
                append(primaryTableText).
                append(" where exists (select * from "). // NOI18N
                append(tableListText).append(whereClause).append(")"). //NOI18N
                append(forUpdateClause);
    }

    /**
     * Generates the table text for the first column of the column list.
     *
     * @param primaryTableText Takes the resulting statement text.
     * @return The table from the first selected column.
     */
    private QueryTable generatePrimaryTableText(StringBuffer primaryTableText) {
        // Get the primary table from the first selected column.
        // TODO: Is the first column always mapped to the primary table?
        QueryTable primaryTable = ((ColumnRef)columns.get(0)).getQueryTable();

        // Generate the table text.
        appendTableText(primaryTableText, primaryTable);

        return primaryTable;
    }

    protected StringBuffer generateColumnText() {
        StringBuffer columnText = new StringBuffer();

        for (int i = 0; i < columns.size(); i++) {
            ColumnRef cr = (ColumnRef) columns.get(i);

            columnText.append("t").append(cr.getQueryTable().getTableIndex()).append("."); // NOI18N
            appendQuotedText(columnText, cr.getName());
            columnText.append(", "); // NOI18N
        }
        columnText.delete(columnText.length() - 2, columnText.length());
        return columnText;
    }

    private String getAggregateText() {
        int aggregateOption = plan.options & RetrieveDescImpl.OPT_AGGREGATE;

        switch (aggregateOption) {
            case RetrieveDescImpl.OPT_AVG:
                return "AVG( "; // NOI18N
            case RetrieveDescImpl.OPT_MIN:
                return "MIN("; // NOI18N
            case RetrieveDescImpl.OPT_MAX:
                return "MAX("; // NOI18N
            case RetrieveDescImpl.OPT_SUM:
                return "SUM("; // NOI18N
            case RetrieveDescImpl.OPT_COUNT:
            case RetrieveDescImpl.OPT_COUNT_PC:
                return "COUNT("; // NOI18N
            default:
                return ""; // NOI18N
        }
    }

    private StringBuffer generateForUpdateClause(boolean updateLockRequired) {
        StringBuffer forUpdateClause = new StringBuffer();

        if (updateLockRequired) {
            // Check if vendor actually supports updatelock
            if (!vendorType.isUpdateLockSupported() ) {
                // Throw an exception user wanted to have update lock
                // But vendor is not supporting it. Do not allow user to proceed
                throw new JDOFatalDataStoreException(I18NHelper.getMessage(messages,
                        "sqlstore.selectstatement.noupdatelocksupport"));// NOI18N
            }

            // generating the ForUpdate Clause
            String vendorForUpdate = vendorType.getForUpdate().trim();
            boolean vendorHasForUpdateClause = (vendorForUpdate.length() != 0);

            if (vendorHasForUpdateClause) {
                forUpdateClause.append(" ").append(vendorForUpdate).append(" ");

                if (vendorType.isLockColumnListSupported()) {
                    for (int i = 0; i < tableList.size(); i++) {
                        QueryTable queryTable = (QueryTable) tableList.get(i);
                        if (isUpdateLockRequired(queryTable)) {
                            TableDesc tableDesc = queryTable.getTableDesc();
                            //Get the first column of primary key
                            ColumnElement ce = (ColumnElement) tableDesc.getKey().getColumns().get(0);
                            forUpdateClause.append("t").append(i).append("."); // NOI18N
                            appendQuotedText(forUpdateClause, ce.getName().getName());
                            forUpdateClause.append(", "); // NOI18N
                        }
                    }
                    // Remove trailing ", "
                    forUpdateClause.delete(forUpdateClause.length() - 2, forUpdateClause.length());
                }
            }
        }

        return forUpdateClause;
    }

    private String getDistinctText(boolean updateLockRequired) {
        String distinctText = ""; // NOI18N

        if ((plan.options & RetrieveDescImpl.OPT_DISTINCT) > 0) {
            if( !updateLockRequired || vendorType.isDistinctSupportedWithUpdateLock()) {
                //Include DISTINCT only if update lock is not required
                //If update lock is required, include DISTINCT only if vendor supports update lock with DISTINCT
                //(e.g. ORACLE throws ORA-01786 if DISTINCT and FOR UPDATE are used in same query

                //For the case where update lock is required and vendor does not support DISTINCT/
                //with update lock, we would do DISTINCT in our code after retrieving the data
                //see SQLStoreManger::retrieve()
                distinctText = "distinct "; // NOI18N
            }
        }
        return distinctText;
    }

    /**
     * Determines if an update lock is required while executing this query.
     *
     * @return True if any of the tables invloved in this query requires
     * update lock
     */
    private boolean isUpdateLockRequired() {
        boolean updateLockRequired = false;

        // TODO: We can optimize this by storing the value in a member variable
        for (int i = 0; i < tableList.size() && !updateLockRequired; i++) {
            QueryTable queryTable = (QueryTable) tableList.get(i);
            updateLockRequired = isUpdateLockRequired(queryTable);
        }

        return updateLockRequired;
    }

    /**
     * Processes Order By constraints and calls the super class
     * method for all other constrains.
     */
    protected void processRootConstraint(ConstraintOperation opNode,
                                         List stack,
                                         StringBuffer whereText) {
        int op = opNode.operation;
        int opInfo = operationFormat(op);

        if ((opInfo & OP_ORDERBY_MASK) > 0) {
            stack.remove(stack.size() - 1);
            ConstraintNode node = (ConstraintNode) stack.get(stack.size() - 1);

            if (!(node instanceof ConstraintField)) {
                throw new JDOFatalInternalException(I18NHelper.getMessage(messages,
                        "core.constraint.needfieldnode")); // NOI18N
            } else {
                processOrderByField((ConstraintFieldDesc) node, op);
                stack.remove(stack.size() - 1);
            }
        } else {
            super.processRootConstraint(opNode, stack, whereText);
        }
    }

    protected void processIrregularOperation(ConstraintOperation opNode,
                                             int opCode,
                                             List stack,
                                             StringBuffer result) {
        switch (opCode) {
            case ActionDesc.OP_EQUIJOIN:
                processJoinOperation((ConstraintJoin)opNode, result);
                break;
            default:
                super.processIrregularOperation(opNode, opCode, stack, result);
        }
    }

    /**
     * Process outer join constraints for this statement.
     *
     * @return A string buffer representing outer join conditions for this
     * statement. Please note that the returned string buffer will have text
     * only for Oracle
     */
    private StringBuffer processOuterJoinConstraints() {
        StringBuffer joinCondition = null;
        final List joinStack = constraint.getOuterJoinConstraints();
        final int joinStackSize = joinStack.size();

        if (joinStackSize > 0) {
            joinCondition = new StringBuffer();
            for (int i = 0; i < joinStackSize; i++) {
                ConstraintJoin joinNode = (ConstraintJoin) joinStack.get(i);
                processJoinOperation(joinNode, joinCondition);
            }
        }
        return joinCondition;
    }

    /**
     * Generates a join condition for specified jnode. Equi joins and native
     * outer joins end  up in the where clause. Ansi compliant outer join
     * conditions end up in the from clause.
     *
     * @param jnode Join constraint.
     * @param whereText String buffer taking the join condition. Generated
     * join condition is appended to this string buffer.
     */
    private void processJoinOperation(ConstraintJoin jnode,
                                      StringBuffer whereText) {
        int opCode = jnode.operation;
        // Generate ANSI outer joins if doAnsiJoin == true,
        // i.e. the vendor has no "native" outer join semantics.
        boolean doAnsiJoin = opCode != ActionDesc.OP_EQUIJOIN
                && !vendorType.isNativeOuterJoin();

        if (doAnsiJoin) {
            generateAnsiJoin(jnode, opCode);
        } else {
            generateJoin(jnode, whereText, opCode);
        }
    }

    /**
     * Generate a "regular" join. The columns for the join condition
     * end up in the where clause. The corresponding tables are added
     * directly to the member variable tableList.
     * Note that this method is normally called to process EQUI- joins
     * only. For databases that are marked for native join sematics,
     * this method will be called to process both EQUI- and OUTER- joins.
     *
     * @param jnode Join constraint.
     * @param whereText String buffer taking the join condition. Generated
     * join condition is appended to this string buffer.
     * @param opCode Join operation.
     */
    private void generateJoin(ConstraintJoin jnode,
                              StringBuffer whereText,
                              int opCode) {

        for (int i = 0; i < jnode.fromColumns.size(); i++) {
            ColumnElement fromColumn = (ColumnElement)jnode.fromColumns.get(i);
            ColumnElement toColumn = (ColumnElement)jnode.toColumns.get(i);
            QueryTable fromTable = findQueryTable(jnode.fromPlan, fromColumn);
            QueryTable toTable = findQueryTable(jnode.toPlan, toColumn);

            addQueryTable(fromTable);
            addQueryTable(toTable);
            toTable.prevTable = null;

            appendJoinCondition(whereText,
                    fromTable, toTable, fromColumn, toColumn,
                    getJoinOperator(opCode));

            if (opCode == ActionDesc.OP_LEFTJOIN ) {
                // Append oracle style (+) or similar.
                whereText.append(vendorType.getLeftJoinPost());
            }
        }
    }

    /**
     * Generates an ANSI compliant join. The columns for the join
     * condition end up in the from clause. The joined tables
     * are added indirectly to the member variable tableList by
     * being added to {@link QueryTable#nextTable} associated with
     * the "from-" table.
     *
     * @param jnode Join constraint.
     * @param opCode Join operation.
     * @see #processFromClause
     */
    private void generateAnsiJoin(ConstraintJoin jnode, int opCode) {

        for (int i = 0; i < jnode.fromColumns.size(); i++) {
            ColumnElement fromColumn = (ColumnElement)jnode.fromColumns.get(i);
            ColumnElement toColumn = (ColumnElement)jnode.toColumns.get(i);
            QueryTable fromTable = findQueryTable(jnode.fromPlan, fromColumn);
            QueryTable toTable = findQueryTable(jnode.toPlan, toColumn);

            // Process the from clause
            processFromClause(fromTable, toTable);

            // Process the on clause.
            if (toTable.onClause == null) {
                toTable.onClause = new StringBuffer();
            }

            appendJoinCondition(toTable.onClause, fromTable, toTable, fromColumn, toColumn, "="); //NOI18N

            fromTable.joinOp = opCode;
        }
    }

    /**
     * Processes specified fromTable and toTable to generate appropriate from
     * clause when table text is generated.
     * toTable is added to fromTable.nextTable if not already present. See
     * {@link #appendAnsiJoinTableText(StringBuffer,QueryTable)} for
     * details on how this is used to generate table text.
     *
     * @param fromTable The from table
     * @param toTable The to table.
     */
    private static void processFromClause(QueryTable fromTable, QueryTable toTable) {

        if (toTable.prevTable != null && toTable.prevTable != fromTable) {
            // TODO
        }

        // TODO:
        // Check that these tables aren't already participating in
        // a join (except to each other).  If either of them are
        // then we will have to make a new table alias for that
        // table and put in an addtional equijoin between the old
        // alias and the new based, of course, on the key columns.

        if (fromTable.nextTable == null) {
            fromTable.nextTable = new ArrayList();
            fromTable.nextTable.add(toTable);
            toTable.prevTable = fromTable;
        } else {
            // Make sure we don't add the same table twice.
            if (!fromTable.nextTable.contains(toTable)) {
                fromTable.nextTable.add(toTable);
                toTable.prevTable = fromTable;
            }
        }
    }

    /**
     * Appends join condition corresponding to specified fromTable, toTable,
     * fromColumn, toColumn, joinOp to result. If result is not empty " and "
     * will be appended to it before appending the join condition.
     *
     * @param result The string buffer to which the condition will be appended.
     * @param fromTable The from table.
     * @param toTable The to table.
     * @param fromColumn The from column.
     * @param toColumn The to column.
     * @param joinOp Join operation.
     */
    private void appendJoinCondition(StringBuffer result,
                                     QueryTable fromTable, QueryTable toTable,
                                     ColumnElement fromColumn, ColumnElement toColumn,
                                     String joinOp) {
        if (result.length() > 0) {
            // Composite fk.
            result.append(" and ");
        }

        result.append("t").append(fromTable.getTableIndex()).append("."); // NOI18N
        appendQuotedText(result, fromColumn.getName().getName());
        result.append(" ").append(joinOp). // NOI18N
           append(" t").append(toTable.getTableIndex()).append("."); // NOI18N
        appendQuotedText(result, toColumn.getName().getName());
    }

    /**
     * Returns join operator for specified operation.
     *
     * @param operation The join operation. Should be one of
     * ActionDesc.OP_EQUIJOIN, ActionDesc.OP_LEFTJOIN or ActionDesc.OP_RIGHTJOIN.
     * @return join operator for specified operation.
     */
    protected String getJoinOperator(int operation) {
        String result = null;

        switch (operation) {
            case ActionDesc.OP_EQUIJOIN:
                result = " = "; // NOI18N
                break;
            case ActionDesc.OP_LEFTJOIN:
                result = vendorType.getLeftJoin();
                break;
            case ActionDesc.OP_RIGHTJOIN:
                result = vendorType.getRightJoin();
                break;
            default:
                throw new JDOFatalInternalException(
                        I18NHelper.getMessage(messages,
                        "core.constraint.illegalop", operation)); // NOI18N
        }
        return result;
    }

    private static QueryTable findQueryTable(QueryPlan plan, ColumnElement ce) {
        QueryTable table = plan.findQueryTable(ce.getDeclaringTable());

        if (table == null) {
            // TODO: throw exception
        }

        return table;
    }

    private String generateTableListText() {
        StringBuffer str = new StringBuffer();

        for (int i = 0; i < tableList.size(); i++) {
            QueryTable t = (QueryTable) tableList.get(i);

            if (t.prevTable == null && t.nextTable == null) {
                appendTableText(str, t);
                str.append(", "); // NOI18N
            } else {
                // Table is part of an outer join list.

                if (t.prevTable == null) {
                    // Beginning of the list.

                    appendAnsiJoinText(str, t);
                } else {
                    // The table is in the "middle" of the list.

                    while (t.prevTable != null) {
                        t = t.prevTable;
                    }

                    if (!tableList.contains(t)) {
                        // Outer join list starts with a join table.
                        // Because join tables aren't in the table list,
                        // they wouldn't be included in the table text.

                        appendAnsiJoinText(str, t);
                    }
                }
            }
        }

        str.delete(str.length() - 2, str.length());

        return str.toString();
    }

    private void appendAnsiJoinText(StringBuffer str, QueryTable t) {
        // TODO: getTableListStart() and getTableListEnd() returns ""
        // for all the databases. Do we need it ?
        str.append(vendorType.getTableListStart());
        appendAnsiJoinTableText(str, t);
        str.append(vendorType.getTableListEnd());
        str.append(", "); // NOI18N
    }

    /**
     * Appends sql text corresponding to specified <code>table</code> to specified text.
     * The linked list starting with table.nextTable is walked recursively
     * to generate join text.
     *
     * @param text The string buffer receiving sql text.
     * @param table Table to be joined.
     */
    private void appendAnsiJoinTableText(StringBuffer text, QueryTable table) {

        if (table.joinOp == ActionDesc.OP_RIGHTJOIN) {
            text.append(vendorType.getRightJoinPre());
        }

        if (table.prevTable == null) {
            appendTableText(text, table);
        }

        for (int i = 0; i < table.nextTable.size(); i++) {
            QueryTable toTable = (QueryTable) table.nextTable.get(i);
            text.append(getJoinOperator(table.joinOp)).append(" "); // NOI18N

            appendTableText(text, toTable);

            if (toTable.onClause != null) {
                text.append(" on "); // NOI18N
                text.append(toTable.onClause);
            }

            if (toTable.nextTable != null) {
                appendAnsiJoinTableText(text, toTable);
            }

            // Note: Since this method is called only for ANSI joins,
            // and only oracle has getLeftJoinPost() defined, we will never
            // append any text through following code.
            if (table.joinOp == ActionDesc.OP_LEFTJOIN) {
                text.append(vendorType.getLeftJoinPost());
            }
        }
    }

    /**
     * Adds a column corresponding to field <code>fieldNode</code> and
     * order operation <code>op</code> to the order constraint.
     *
     * @param fieldNode Constraint on a field name of a persistence capable class.
     * @param op Order operation.
     */
    private void processOrderByField(ConstraintFieldDesc fieldNode, int op) {
        QueryPlan thePlan = getOriginalPlan(fieldNode);
        StringBuffer orderText = new StringBuffer();

        generateColumnText(fieldNode.desc, thePlan, orderText);

        if (op == ActionDesc.OP_ORDERBY_DESC) {
            orderText.append(" desc"); // NOI18N
        }

        if (orderClause.length() > 0) {
            orderText.append(", "); // NOI18N
            orderText.append(orderClause);
        }
        orderClause = orderText;
    }

    /**
     * Binds input valus corrsponding to this <code>Statement</code> object to
     * database statement s.
     * @param s The database statement.
     * @param parameters If an InputValue to be bound is a parameter, the actual
     * value is contained in this.
     * @throws SQLException
     */
    public void bindInputValues(DBStatement s, ValueFetcher parameters)
            throws SQLException {
        for (int i = 0, size = inputDesc.values.size(); i < size; i++) {
            InputValue inputVal = (InputValue) inputDesc.values.get(i);
            s.bindInputColumn(i + 1, getInputValue(inputVal, parameters),
                    inputVal.getColumnElement(), vendorType);
        }
    }

    /**
     * Get Input values to be bound to this statement.
     * @param parameters If an InputValue to be bound is a parameter, the actual
     * value is contained in this.
     * @return An Object array containing input values to be bound to this statement.
     */
    private Object[] getInputValues(ValueFetcher parameters) {
        final int size = inputDesc.values.size();
        Object[] inputValues = new Object[size];
        for (int i = 0; i < size; i++) {
            InputValue inputValue = (InputValue) inputDesc.values.get(i);
            inputValues[i] = getInputValue(inputValue, parameters);
        }
        return inputValues;
    }

    /**
     * Gets formatted sql text corrsponding to this statement object. The text
     * also contains values for input to the statement.
     * @param parameters The input paramters to this statement.
     * @return formatted sql text corrsponding to this statement object.
     */
    public String getFormattedSQLText(ValueFetcher parameters) {
        return formatSqlText(getText(), getInputValues(parameters)) ;
    }

    /**
     * Gets actual value corresponding to <code>inputVal</code>. If
     * <code>inputVal</code> is an instanceof InputValue then value contained
     * in inputVal is returned. If <code>inputVal</code> is an instanceof
     * InputParamValue then value returned is obtained from <code>parameters
     * </code>.
     * @param inputVal The input value.
     * @param parameters The parameters.
     * @return Appropriate value as described above.
     */
    private static Object getInputValue(InputValue inputVal,
            ValueFetcher parameters) {
        Object val;
        if (inputVal instanceof InputParamValue) {
            int paramIndex = ((InputParamValue) inputVal).getParamIndex().intValue();
            val = parameters.getValue(paramIndex);
        }
        else {
            val = inputVal.getValue();
        }
        return val;
    }

}
