/**
 * Redistribution and use of this software and associated documentation
 * ("Software"), with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * 1. Redistributions of source code must retain copyright
 *    statements and notices.  Redistributions must also contain a
 *    copy of this document.
 *
 * 2. 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.
 *
 * 3. The name "Exolab" must not be used to endorse or promote
 *    products derived from this Software without prior written
 *    permission of Intalio, Inc.  For written permission,
 *    please contact info@exolab.org.
 *
 * 4. Products derived from this Software may not be called "Exolab"
 *    nor may "Exolab" appear in their names without prior written
 *    permission of Intalio, Inc. Exolab is a registered
 *    trademark of Intalio, Inc.
 *
 * 5. Due credit should be given to the Exolab Project
 *    (http://www.exolab.org/).
 *
 * THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESSED 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
 * INTALIO, INC. OR ITS 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.
 *
 * Copyright 1999 (C) Intalio, Inc. All Rights Reserved.
 *
 * $Id: SqlBindParser.java 7951 2008-10-09 20:52:17Z wguttmn $
 */
package org.exolab.castor.jdo.engine;

import java.sql.PreparedStatement;
import java.sql.SQLException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.castor.core.util.Messages;

/**
 * Utility class to parse an SQL or OQL expression for bind variables
 * Bind variables are subexpressions of the form "$n", where n is a
 * positive integer number. 
 * 
 * To parse the expression, call SqlBindParser.next() in a loop
 * until no more bind variable can be found. Each call moves on to
 * the next bind variable and returns true if another could be found.
 *  
 * Inside the loop call SqlBindParser.getBindExpr() to access the
 * current bind variable expression. ("$1", "$2", ...)
 * 
 * SqlBindParser.getParamNumber() can be used to read the parameter
 * number (1, 2, ...).
 * 
 * If you are interested in the remainder of the expression string,
 * just call getLastExpr() to get the last processed substring.
 *
 * For example, when parsing the expression
 * "select * from x where id between $1 and $2"
 * this gives you the following function returns:
 * next()           -> true
 * getLastExpr()    -> "select * from x where id between "
 * getBindExpr()    -> "$1"
 * getParamNumber() -> 1
 * next()           -> true
 * getLastExpr()    -> " and "
 * getBindExpr()    -> "$2"
 * getParamNumber() -> 2
 * next()           -> false
 * getLastExpr()    -> ""
 *
 * @author <a href="mailto:martin-fuchs AT gmx DOT net"> Martin Fuchs</a>
 */
public final class SqlBindParser {
    /** The <a href="http://jakarta.apache.org/commons/logging/">Jakarta
     *  Commons Logging</a> instance used for all logging. */
    private static Log _log = LogFactory.getFactory().getInstance(SqlBindParser.class);

    /** Complete SQL expression to be parsed. */
    private String _sql;

    /** Length of _sql string. */
    private int _length;

    /** Current parse position. */
    private int _pos;

    /** Last parse position. */
    private int _lastPos;

    /** Position of the current bind variable. */
    private int _bindPos;

    /**
     * Create a new SqlBindParser instance to parse the expression in 'sql'.
     *
     * @param sql expression to be parsed
     */
    public SqlBindParser(final String sql) {
        _sql = sql;
        _length = _sql.length();
        _pos = 0;
        _lastPos = 0;
        _bindPos = 0;
    }

    /**
     * Move on to the next bind variable in '_sql'.
     *
     * @return true, if an bind variable could be found 
     */
    public boolean next() {
        _lastPos = _pos;

         // search for the begin of the next bind variable
        int pos = _pos;
        while (pos < _length) {
            char c = _sql.charAt(pos);

            switch(c) {
              case '\'':    // character constant
              case '\"':    // string constant
                while (pos < _length) {
                    pos = _sql.indexOf(c, pos + 1);
                    if (pos == -1) {    // unexpected end of the constant?
                        pos = _length;
                        break;
                    } else if (_sql.charAt(pos - 1) != '\\') {
                        // handle escape characters
                        // end of the constant
                        break;
                    }
                }
                break;
              case '?':     // bind variable
                _bindPos = pos;

                // search for the end of the bind variable
                do {
                    pos++;
                } while (pos < _length && Character.isDigit(_sql.charAt(pos)));

               _pos = pos;

               return true;
              default:
                  break;
            }

            pos++;
        }

        _bindPos = _length;
        _pos = _length;

        return false;
    }

    /**
     * Returns the expression substring beginning after the
     * last processed bind variable and ending just before the
     * current bind variable.
     *
     * @return last expression substring
     */
    public String getLastExpr() {
        return _sql.substring(_lastPos, _bindPos);
    }

    /**
     * Returns the current bind variable expression, e.g. "$1".
     *
     * @return current bind variable expression
     */
    public String getBindExpr() {
        return _sql.substring(_bindPos, _pos);
    }

    /**
     * Returns the parameter number of the current bind variable,
     * for example 1 a "$1" bind variable.
     * If an un-numbered bind variable "$" is found, 0 is returned.  
     * 
     * @return parameter number of current bind variable
     */
    public int getParamNumber() {
        int idx = _bindPos + 1;

        return (idx < _pos) 
               ? Integer.parseInt(_sql.substring(idx, _pos))
               : 0;     // no numbered bind variable
    }


    /**
     * Creates a SQL statement from pre_sql, replacing bind expressions like "?1" by "?".
     * 
     * @param preSQL SQL statement string with bind variables of the form "?1".
     * @return SQL statement string with bind variables of the form "?".
     */
    public static String getJdbcSql(final String preSQL) {
        StringBuffer sb = new StringBuffer();
        SqlBindParser parser = new SqlBindParser(preSQL);

        while (parser.next()) {
            sb.append(parser.getLastExpr());
            sb.append(JDBCSyntax.PARAMETER);
        }

        sb.append(parser.getLastExpr());

        return sb.toString();
    }

    /**
     * Binds values to prepared SQL statement using the given
     * sql string as reference for the bind variable order.
     * @param stmt JDBC statement
     * @param preSQL SQL statement string with bind variables of the form "?1"
     * @param values array of bind values
     * @throws SQLException
     */
    public static void bindJdbcValues(final PreparedStatement stmt, final String preSQL,
            final Object[] values) throws SQLException {
        SqlBindParser parser = new SqlBindParser(preSQL);

        for (int i = 1; parser.next(); ++i) {
            int bindNum = parser.getParamNumber();

            if (bindNum == 0) {
                // handle CALL SQL statements with unnumbered bind variables
                bindNum = i;
            }

            Object value = values[bindNum - 1];

            if (_log.isDebugEnabled()) {
                if (value == null) {
                    _log.debug(Messages.format("jdo.bindSqlNull", Integer.toString(i)));
                } else {
                    _log.debug(Messages.format("jdo.bindSql", Integer.toString(i),
                            value, value.getClass().getName()));
                }
            }

            stmt.setObject(i, value);
        }
    }
}
