/*
* Copyright (c) 1998-2011 Caucho Technology -- all rights reserved
*
* This file is part of Resin(R) Open Source
*
* Each copy or derived work must preserve the copyright notice and this
* notice unmodified.
*
* Resin Open Source is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* Resin Open Source is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
* of NON-INFRINGEMENT. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License
* along with Resin Open Source; if not, write to the
*
* Free Software Foundation, Inc.
* 59 Temple Place, Suite 330
* Boston, MA 02111-1307 USA
*
* @author Charles Reich
*/
package com.caucho.quercus.lib.db;
import com.caucho.quercus.env.BooleanValue;
import com.caucho.quercus.env.Env;
import com.caucho.quercus.env.NullValue;
import com.caucho.quercus.env.StringValue;
import com.caucho.quercus.env.UnsetValue;
import com.caucho.quercus.env.Value;
import com.caucho.quercus.env.Var;
import com.caucho.quercus.env.StringValue;
import com.caucho.util.L10N;
import com.caucho.util.Log;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Represents a JDBC Statement value.
*/
public class JdbcStatementResource {
private static final Logger log = Log.open(JdbcStatementResource.class);
private static final L10N L = new L10N(JdbcStatementResource.class);
private JdbcConnectionResource _conn;
private ResultSet _rs;
private String _query;
private PreparedStatement _stmt;
private ResultSetMetaData _metaData;
private JdbcResultResource _resultResource = null;
private char[] _types;
private Value[] _params;
private Value[] _results;
private String _errorMessage = "";
private int _errorCode;
// Statement type
// (SELECT, UPDATE, DELETE, INSERT, CREATE,
// DROP, ALTER, BEGIN, DECLARE, UNKNOWN)
private String _stmtType;
/**
* Constructor for JdbcStatementResource
*
* @param connV a JdbcConnectionResource connection
*/
public JdbcStatementResource(JdbcConnectionResource connV)
{
_conn = connV;
}
/**
* Creates _types and _params array for this prepared statement.
*
* @param types = string of i,d,s,b (ie: "idds")
* @param params = array of values (probably Vars)
* @return true on success ir false on failure
*/
protected boolean bindParams(Env env,
String types,
Value[] params)
{
// This will create the _types and _params arrays
// for this prepared statement.
final int size = types.length();
// Check to see that types and params have the same length
if (params.length == 0 || size != params.length) {
env.warning(L.l("number of types does not match number of parameters"));
return false;
}
// Check to see that types only contains i,d,s,b
for (int i = 0; i < size; i++) {
if ("idsb".indexOf(types.charAt(i)) < 0) {
env.warning(L.l("invalid type string {0}", types));
return false;
}
}
_types = new char[size];
_params = new Value[size];
for (int i = 0; i < size; i++) {
_types[i] = types.charAt(i);
_params[i] = params[i];
}
return true;
}
/**
* Associate (bind) columns in the result set to variables.
* <p/>
* NB: we assume that the statement has been executed and
* compare the # of outParams w/ the # of columns in the
* resultset because we cannot know in advance how many
* columns "SELECT * FROM TableName" can return.
* <p/>
* PHP 5.0 seems to provide some rudimentary checking on # of
* outParams even before the statement has been executed
* and only issues a warning in the case of "SELECT * FROM TableName".
* <p/>
* Our implementation REQUIRES the execute happen first.
*
* @param env the PHP executing environment
* @param outParams the output variables
* @return true on success or false on failure
*/
public boolean bindResults(Env env,
Value[] outParams)
{
final int size = outParams.length;
int numColumns;
try {
ResultSetMetaData md = getMetaData();
numColumns = md.getColumnCount();
} catch (SQLException e) {
log.log(Level.FINE, e.toString(), e);
return false;
}
for (int i = 0; i < size; i++) {
Value val = outParams[i];
if (! (val instanceof Var)) {
env.error(L.l("Only variables can be passed by reference"));
return false;
}
}
if ((size == 0) || (size != numColumns)) {
env.warning(
L.l("number of bound variables does not equal number of columns"));
return false;
}
_results = new Value[size];
System.arraycopy(outParams, 0, _results, 0, size);
return true;
}
/**
* Closes the result set, if any, and closes this statement.
*/
public void close()
{
try {
ResultSet rs = _rs;
_rs = null;
if (rs != null)
rs.close();
if (_stmt != null)
_stmt.close();
} catch (SQLException e) {
_errorMessage = e.getMessage();
_errorCode = e.getErrorCode();
log.log(Level.FINE, e.toString(), e);
}
}
/**
* Advance the cursor the number of rows given by offset.
*
* @param offset the number of rows to move the cursor
* @return true on success or false on failure
*/
protected boolean dataSeek(int offset)
{
return JdbcResultResource.setRowNumber(_rs, offset);
}
/**
* Returns the error number for the last error.
*
* @return the error number
*/
public int errorCode()
{
return _errorCode;
}
/**
* Returns the error message for the last error.
*
* @return the error message
*/
public String errorMessage()
{
return _errorMessage;
}
/**
* Executes a prepared Query.
*
* @param env the PHP executing environment
* @return true on success or false on failure
*/
public boolean execute(Env env)
{
try {
if (_types != null) {
int size = _types.length;
for (int i = 0; i < size; i++) {
switch (_types[i]) {
case 'i':
_stmt.setInt(i + 1, _params[i].toInt());
break;
case 'd':
_stmt.setDouble(i + 1, _params[i].toDouble());
break;
// XXX: blob needs to be redone
// Currently treated as a string
case 'b':
_stmt.setString(i + 1, _params[i].toString());
break;
case 's':
_stmt.setString(i + 1, _params[i].toString());
break;
default:
break;
}
}
}
return executeStatement();
} catch (SQLException e) {
env.warning(L.l(e.toString()));
log.log(Level.FINE, e.toString(), e);
_errorMessage = e.getMessage();
_errorCode = e.getErrorCode();
return false;
}
}
/**
* Executes underlying statement
* Known subclasses: see PostgresStatement.execute
*/
protected boolean executeStatement()
throws SQLException
{
try {
if (_stmt.execute()) {
_conn.setAffectedRows(0);
_rs = _stmt.getResultSet();
} else {
_conn.setAffectedRows(_stmt.getUpdateCount());
}
return true;
} catch (SQLException e) {
_errorMessage = e.getMessage();
_errorCode = e.getErrorCode();
throw e;
}
}
/**
* Fetch results from a prepared statement into bound variables.
*
* @return true on success, false on error, null if no more rows
*/
public Value fetch(Env env)
{
try {
if (_rs == null)
return NullValue.NULL;
if (_rs.next()) {
if (_metaData == null)
_metaData = _rs.getMetaData();
JdbcResultResource resultResource = getResultMetadata();
int size = _results.length;
for (int i = 0; i < size; i++) {
_results[i].set(_resultResource.getColumnValue(
env, _rs, _metaData, i + 1));
}
return BooleanValue.TRUE;
} else {
return NullValue.NULL;
}
} catch (SQLException e) {
log.log(Level.FINE, e.toString(), e);
return BooleanValue.FALSE;
}
}
/**
* Frees the associated result.
*
* @return true on success or false on failure
*/
public boolean freeResult()
{
try {
ResultSet rs = _rs;
_rs = null;
if (rs != null)
rs.close();
if (_resultResource != null) {
_resultResource.close();
_resultResource = null;
}
return true;
} catch (SQLException e) {
_errorMessage = e.getMessage();
_errorCode = e.getErrorCode();
log.log(Level.FINE, e.toString(), e);
return false;
}
}
/**
* Returns the meta data for corresponding to the current result set.
*
* @return the result set meta data
*/
protected ResultSetMetaData getMetaData()
throws SQLException
{
if (_metaData == null)
_metaData = _rs.getMetaData();
return _metaData;
}
/**
* Returns the number of rows in the result set.
*
* @return the number of rows in the result set
*/
public int getNumRows()
throws SQLException
{
if (_rs != null)
return JdbcResultResource.getNumRows(_rs);
else
return 0;
}
/**
* Returns the internal prepared statement.
*
* @return the internal prepared statement
*/
protected PreparedStatement getPreparedStatement()
{
return _stmt;
}
/**
* Resets _fieldOffset in _resultResource
*
* @return null if _resultResource == null, otherwise _resultResource
*/
public JdbcResultResource getResultMetadata()
{
if (_resultResource != null) {
_resultResource.setFieldOffset(0);
return _resultResource;
}
if (_stmt == null || _rs == null)
return null;
_resultResource
= new JdbcResultResource(_conn.getEnv(), _stmt, _rs, _conn);
return _resultResource;
}
/**
* Returns the internal result set.
*
* @return the internal result set
*/
protected ResultSet getResultSet()
{
return _rs;
}
/**
* Returns the underlying SQL connection
* associated to this statement.
*/
protected Connection getJavaConnection()
throws SQLException
{
return validateConnection().getJavaConnection();
}
/**
* Returns this statement type.
*
* @return this statement type:
* SELECT, UPDATE, DELETE, INSERT, CREATE, DROP,
* ALTER, BEGIN, DECLARE, or UNKNOWN.
*/
public String getStatementType()
{
// Oracle Statement type
// Also used internally in Postgres (see PostgresModule)
// (SELECT, UPDATE, DELETE, INSERT, CREATE, DROP,
// ALTER, BEGIN, DECLARE, UNKNOWN)
_stmtType = _query;
_stmtType = _stmtType.replaceAll("\\s+.*", "");
if (_stmtType.length() == 0)
_stmtType = "UNKNOWN";
else {
_stmtType = _stmtType.toUpperCase(Locale.ENGLISH);
String s = _stmtType.replaceAll(
"(SELECT|UPDATE|DELETE|INSERT|CREATE|DROP|ALTER|BEGIN|DECLARE)", "");
if (! s.equals(""))
_stmtType = "UNKNOWN";
}
return _stmtType;
}
/**
* Counts the number of parameter markers in the query string.
*
* @return the number of parameter markers in the query string
*/
public int paramCount()
{
if (_query == null)
return -1;
int count = 0;
int length = _query.length();
boolean inQuotes = false;
char c;
for (int i = 0; i < length; i++) {
c = _query.charAt(i);
if (c == '\\') {
if (i < length - 1)
i++;
continue;
}
if (inQuotes) {
if (c == '\'')
inQuotes = false;
continue;
}
if (c == '\'') {
inQuotes = true;
continue;
}
if (c == '?') {
count++;
}
}
return count;
}
/**
* Prepares this statement with the given query.
*
* @param query SQL query
* @return true on success or false on failure
*/
public boolean prepare(Env env, StringValue query)
{
try {
if (_stmt != null)
_stmt.close();
_query = query.toString();
if (_query.length() == 0)
return false;
Connection conn = _conn.getConnection(env);
if (conn == null)
return false;
if (this instanceof OracleStatement) {
_stmt = conn.prepareCall(_query,
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_READ_ONLY);
} else if (_conn.isSeekable()) {
_stmt = conn.prepareStatement(_query,
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_READ_ONLY);
} else {
_stmt = conn.prepareStatement(_query);
}
return true;
} catch (SQLException e) {
log.log(Level.FINE, e.toString(), e);
_errorMessage = e.getMessage();
_errorCode = e.getErrorCode();
return false;
}
}
/**
* Prepares statement with the given query.
*
* @param query SQL query
* @return true on success or false on failure
*/
public boolean prepareStatement(Env env, String query)
{
try {
if (_stmt != null)
_stmt.close();
_query = query;
Connection conn = _conn.getConnection(env);
if (conn == null)
return false;
if (this instanceof OracleStatement) {
_stmt = conn.prepareCall(query,
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_READ_ONLY);
} else {
_stmt = conn.prepareStatement(query,
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_READ_ONLY);
}
return true;
} catch (SQLException e) {
log.log(Level.FINE, e.toString(), e);
_errorMessage = e.getMessage();
_errorCode = e.getErrorCode();
return false;
}
}
/**
* Returns a parameter value
* Known subclasses: see PostgresStatement.execute
*/
protected Value getParam(int i)
{
if (i >= _params.length)
return UnsetValue.UNSET;
return _params[i];
}
/**
* Returns the number of parameters available to binding
* Known subclasses: see PostgresStatement.execute
*/
protected int getParamLength()
{
return _params.length;
}
/**
* Changes the internal statement.
*/
protected void setPreparedStatement(PreparedStatement stmt)
{
_stmt = stmt;
}
/**
* Changes the internal result set.
*/
protected void setResultSet(ResultSet rs)
{
_rs = rs;
}
/**
* Returns the number of fields in the result set.
*
* @param env the PHP executing environment
* @return the number of fields in the result set
*/
public int getFieldCount()
{
if (_resultResource == null)
return 0;
return _resultResource.getFieldCount();
}
/**
* Sets the given parameter
* Known subclasses: see PostgresStatement.execute
*/
protected void setObject(int i, Object param)
throws Exception
{
try {
// See php/4358, php/43b8, php/43d8, and php/43p8.
java.sql.ParameterMetaData pmd = _stmt.getParameterMetaData();
int type = pmd.getParameterType(i);
switch (type) {
case Types.OTHER:
{
// See php/43b8
String typeName = pmd.getParameterTypeName(i);
if (typeName.equals("interval")) {
_stmt.setObject(i, param);
} else {
Class cl = Class.forName("org.postgresql.util.PGobject");
Constructor constructor = cl.getDeclaredConstructor(null);
Object object = constructor.newInstance();
Method method = cl.getDeclaredMethod(
"setType", new Class[] {String.class});
method.invoke(object, new Object[] {typeName});
method = cl.getDeclaredMethod(
"setValue", new Class[] {String.class});
method.invoke(object, new Object[] {param});
_stmt.setObject(i, object, type);
}
break;
}
case Types.DOUBLE:
{
// See php/43p8.
String typeName = pmd.getParameterTypeName(i);
if (typeName.equals("money")) {
String s = param.toString();
if (s.length() == 0) {
throw new IllegalArgumentException(
L.l("argument `{0}' cannot be empty", param));
} else {
String money = s;
if (s.charAt(0) == '$')
s = s.substring(1);
else
money = "$" + money;
try {
// This will throw an exception if not double while
// trying to setObject() would not. The error would
// come late, otherwise. See php/43p8.
Double.parseDouble(s);
} catch (Exception ex) {
throw new IllegalArgumentException(L.l(
"cannot convert argument `{0}' to money", param));
}
Class cl = Class.forName("org.postgresql.util.PGmoney");
Constructor constructor = cl.getDeclaredConstructor(
new Class[] {String.class});
Object object = constructor.newInstance(new Object[] {money});
_stmt.setObject(i, object, Types.OTHER);
break;
}
}
// else falls to default case
}
default:
_stmt.setObject(i, param, type);
}
}
catch (SQLException e) {
_errorMessage = e.getMessage();
_errorCode = e.getErrorCode();
throw e;
}
catch (Exception e) {
_stmt.clearParameters();
throw e;
}
}
/**
* Returns a string representation for this object.
*
* @return the string representation for this object
*/
public String toString()
{
return getClass().getName() + "[" + _conn + "]";
}
/**
* Validates the connection resource.
*
* @return the validated connection resource
*/
public JdbcConnectionResource validateConnection()
{
return _conn;
}
}