/**
* 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$
*/
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);
}
}
}