/* * Copyright (c) 1998-2010 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 Scott Ferguson */ package com.caucho.quercus.lib.db; import com.caucho.quercus.UnimplementedException; import com.caucho.quercus.annotation.Optional; import com.caucho.quercus.annotation.ReturnNullAsFalse; import com.caucho.quercus.env.ArrayValue; import com.caucho.quercus.env.ArrayValueImpl; import com.caucho.quercus.env.BooleanValue; import com.caucho.quercus.env.Env; import com.caucho.quercus.env.EnvCleanup; import com.caucho.quercus.env.LongValue; import com.caucho.quercus.env.StringValue; import com.caucho.quercus.env.Value; import com.caucho.util.L10N; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.sql.DataSource; import java.io.InputStream; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Properties; import java.util.logging.Level; import java.util.logging.Logger; /** * PDO object oriented API facade. */ public class PDO implements EnvCleanup { private static final Logger log = Logger.getLogger(PDO.class.getName()); private static final L10N L = new L10N(PDO.class); public static final int ATTR_AUTOCOMMIT = 0; public static final int ATTR_PREFETCH = 1; public static final int ATTR_TIMEOUT = 2; public static final int ATTR_ERRMODE = 3; public static final int ATTR_SERVER_VERSION = 4; public static final int ATTR_CLIENT_VERSION = 5; public static final int ATTR_SERVER_INFO = 6; public static final int ATTR_CONNECTION_STATUS = 7; public static final int ATTR_CASE = 8; public static final int ATTR_CURSOR_NAME = 9; public static final int ATTR_CURSOR = 10; public static final int ATTR_ORACLE_NULLS = 11; public static final int ATTR_PERSISTENT = 12; public static final int ATTR_STATEMENT_CLASS = 13; public static final int ATTR_FETCH_TABLE_NAMES = 14; public static final int ATTR_FETCH_CATALOG_NAMES = 15; public static final int ATTR_DRIVER_NAME = 16; public static final int ATTR_STRINGIFY_FETCHES = 17; public static final int ATTR_MAX_COLUMN_LEN = 18; public static final int CASE_NATURAL = 0; public static final int CASE_UPPER = 1; public static final int CASE_LOWER = 2; public static final int CURSOR_FWDONLY = 0; public static final int CURSOR_SCROLL = 1; public static final String ERR_NONE = "00000"; public static final int ERRMODE_SILENT = 0; public static final int ERRMODE_WARNING = 1; public static final int ERRMODE_EXCEPTION = 2; public static final int FETCH_LAZY = 1; public static final int FETCH_ASSOC = 2; public static final int FETCH_NUM = 3; public static final int FETCH_BOTH = 4; public static final int FETCH_OBJ = 5; public static final int FETCH_BOUND = 6; public static final int FETCH_COLUMN = 7; public static final int FETCH_CLASS = 8; public static final int FETCH_INTO = 9; public static final int FETCH_FUNC = 10; public static final int FETCH_NAMED = 11; public static final int FETCH_GROUP = 0x00010000; public static final int FETCH_UNIQUE = 0x00030000; public static final int FETCH_CLASSTYPE = 0x00040000; public static final int FETCH_SERIALIZE = 0x00080000; public static final int FETCH_ORI_NEXT = 0; public static final int FETCH_ORI_PRIOR = 1; public static final int FETCH_ORI_FIRST = 2; public static final int FETCH_ORI_LAST = 3; public static final int FETCH_ORI_ABS = 4; public static final int FETCH_ORI_REL = 5; public static final int NULL_NATURAL = 0; public static final int NULL_EMPTY_STRING = 1; public static final int NULL_TO_STRING = 2; public static final int PARAM_NULL = 0; public static final int PARAM_INT = 1; public static final int PARAM_STR = 2; public static final int PARAM_LOB = 3; public static final int PARAM_STMT = 4; public static final int PARAM_BOOL = 5; public static final int PARAM_INPUT_OUTPUT = 0x80000000; // MySQL-specific constants public static final int MYSQL_ATTR_USE_BUFFERED_QUERY = 1000; public static final int MYSQL_ATTR_LOCAL_INFILE = 1001; public static final int MYSQL_ATTR_INIT_COMMAND = 1002; public static final int MYSQL_ATTR_READ_DEFAULT_FILE = 1003; public static final int MYSQL_ATTR_READ_DEFAULT_GROUP = 1004; public static final int MYSQL_ATTR_MAX_BUFFER_SIZE = 1005; public static final int MYSQL_ATTR_COMPRESS = 1006; public static final int MYSQL_ATTR_DIRECT_QUERY = 1007; public static final int MYSQL_ATTR_FOUND_ROWS = 1008; public static final int MYSQL_ATTR_IGNORE_SPACE = 1009; public static final Value MYSQL_ATTR_INIT_COMMAND_V = StringValue.create(MYSQL_ATTR_INIT_COMMAND); public static final Value ATTR_ERRMODE_V = StringValue.create(ATTR_ERRMODE); private final Env _env; private final String _dsn; private String _user; private String _password; private final PDOError _error; private Connection _conn; private Statement _lastStatement; private PDOStatement _lastPDOStatement; private String _lastInsertId; private boolean _inTransaction; private static String ENCODING = "ISO8859_1"; private final static Properties driverList = new Properties(); static { try { InputStream is = PDO.class.getResourceAsStream("pdo.properties"); driverList.load(is); is.close(); } catch (Exception e) { } } public PDO(Env env, String dsn, @Optional("null") String user, @Optional("null") String password, @Optional ArrayValue options) { _env = env; _dsn = dsn; _user = user; _password = password; _error = new PDOError(_env); // TODO: following would be better as annotation on destroy() method _env.addCleanup(this); try { DataSource ds = getDataSource(env, dsn); if (ds == null) { env.warning(L.l("'{0}' is an unknown PDO data source.", dsn)); } else if (_user != null && !"".equals(_user)) { _conn = ds.getConnection(_user, _password); } else { _conn = ds.getConnection(); } // TODO: move this code into a parseOptions() function if (options != null) { // gh#29 if (options.keyExists(ATTR_ERRMODE_V)) { _error.setErrmode(options.get(ATTR_ERRMODE_V).toInt()); } if (options.keyExists(MYSQL_ATTR_INIT_COMMAND_V)) { exec(options.get(MYSQL_ATTR_INIT_COMMAND_V).toString()); } } } catch (RuntimeException e) { throw e; } catch (Exception e) { env.warning(e.getMessage(), e); _error.error(e); } } /** * Starts a transaction. */ public boolean beginTransaction() { if (_conn == null) { return false; } if (_inTransaction) { return false; } _inTransaction = true; try { _conn.setAutoCommit(false); return true; } catch (SQLException e) { _error.error(e); return false; } } private void closeStatements() { Statement lastStatement = _lastStatement; _lastInsertId = null; _lastStatement = null; _lastPDOStatement = null; try { if (lastStatement != null) { lastStatement.close(); } } catch (Throwable t) { log.log(Level.WARNING, t.toString(), t); } } /** * Commits a transaction. */ public boolean commit() { if (_conn == null) { return false; } if (!_inTransaction) { return false; } _inTransaction = false; boolean result = false; try { _conn.commit(); _conn.setAutoCommit(true); return true; } catch (SQLException e) { _error.error(e); } return result; } public void close() { cleanup(); } /** * Implements the EnvCleanup interface. */ @Override public void cleanup() { Connection conn = _conn; _conn = null; closeStatements(); if (conn != null) { try { conn.close(); } catch (SQLException e) { log.log(Level.WARNING, e.toString(), e); } } } public String errorCode() { return _error.errorCode(); } public ArrayValue errorInfo() { return _error.errorInfo(); } /** * Executes a statement, returning the number of rows. */ public int exec(String query) throws SQLException { if (_conn == null) { return -1; } closeStatements(); Statement stmt = null; int rowCount; try { stmt = _conn.createStatement(); stmt.setEscapeProcessing(false); if (stmt.execute(query)) { ResultSet resultSet = null; try { resultSet = stmt.getResultSet(); resultSet.last(); rowCount = resultSet.getRow(); _lastStatement = stmt; stmt = null; } finally { try { if (resultSet != null) { resultSet.close(); } } catch (SQLException e) { log.log(Level.FINER, e.toString(), e); } } } else { rowCount = stmt.getUpdateCount(); _lastStatement = stmt; stmt = null; } } catch (SQLException e) { _error.error(e); return -1; } finally { try { if (stmt != null) { stmt.close(); } } catch (SQLException e) { log.log(Level.FINER, e.toString(), e); } } return rowCount; } public Value getAttribute(int attribute) { switch (attribute) { case ATTR_AUTOCOMMIT: return BooleanValue.create(getAutocommit()); case ATTR_CASE: return LongValue.create(getCase()); case ATTR_CLIENT_VERSION: throw new UnimplementedException(); case ATTR_CONNECTION_STATUS: throw new UnimplementedException(); case ATTR_DRIVER_NAME: throw new UnimplementedException(); case ATTR_ERRMODE: return LongValue.create(_error.getErrmode()); case ATTR_ORACLE_NULLS: return LongValue.create(getOracleNulls()); case ATTR_PERSISTENT: return BooleanValue.create(getPersistent()); case ATTR_PREFETCH: return LongValue.create(getPrefetch()); case ATTR_SERVER_INFO: return StringValue.create(getServerInfo()); case ATTR_SERVER_VERSION: return StringValue.create(getServerVersion()); case ATTR_TIMEOUT: return LongValue.create(getTimeout()); default: _error.unsupportedAttribute(attribute); // TODO: check what php does return BooleanValue.FALSE; } } public static ArrayValue getAvailableDrivers() { ArrayValue array = new ArrayValueImpl(); array.put("mysql"); array.put("pgsql"); array.put("java"); array.put("jdbc"); return array; } /** * Returns the auto commit value for the connection. */ private boolean getAutocommit() { if (_conn == null) { return true; } try { return _conn.getAutoCommit(); } catch (SQLException e) { _error.error(e); return true; } } public int getCase() { throw new UnimplementedException(); } public int getOracleNulls() { throw new UnimplementedException(); } private boolean getPersistent() { return true; } private int getPrefetch() { throw new UnimplementedException(); } private String getServerInfo() { throw new UnimplementedException(); } // TODO: might be int return private String getServerVersion() { throw new UnimplementedException(); } private int getTimeout() { throw new UnimplementedException(); } public String lastInsertId(@Optional String name) { if (!(name == null || name.length() == 0)) { throw new UnimplementedException("lastInsertId with name"); } if (_lastInsertId != null) { return _lastInsertId; } String lastInsertId = null; if (_lastPDOStatement != null) { lastInsertId = _lastPDOStatement.lastInsertId(name); } else if (_lastStatement != null) { ResultSet resultSet = null; try { resultSet = _lastStatement.getGeneratedKeys(); if (resultSet.next()) { lastInsertId = resultSet.getString(1); } } catch (SQLException ex) { _error.error(ex); } finally { try { if (resultSet != null) { resultSet.close(); } } catch (SQLException ex) { log.log(Level.WARNING, ex.toString(), ex); } } } _lastInsertId = lastInsertId == null ? "0" : lastInsertId; return _lastInsertId; } /** * Prepares a statement for execution. */ @ReturnNullAsFalse public PDOStatement prepare(String statement, @Optional ArrayValue driverOptions) { if (_conn == null) { return null; } try { closeStatements(); PDOStatement pdoStatement = new PDOStatement(_env, _conn, statement, true, driverOptions); _lastPDOStatement = pdoStatement; return pdoStatement; } catch (SQLException e) { _error.error(e); return null; } } /** * Queries the database */ public Value query(String query) { if (_conn == null) { return BooleanValue.FALSE; } try { closeStatements(); PDOStatement pdoStatement = new PDOStatement( _env, _conn, query, false, null); _lastPDOStatement = pdoStatement; return _env.wrapJava(pdoStatement); } catch (SQLException e) { _error.error(e); return BooleanValue.FALSE; } } /** * Quotes the string */ public String quote(String query, @Optional int parameterType) { return "'" + real_escape_string(query) + "'"; } /** * Escapes the string. */ public String real_escape_string(String str) { StringBuilder buf = new StringBuilder(); final int strLength = str.length(); for (int i = 0; i < strLength; i++) { char c = str.charAt(i); switch (c) { case '\u0000': buf.append('\\'); buf.append('\u0000'); break; case '\n': buf.append('\\'); buf.append('n'); break; case '\r': buf.append('\\'); buf.append('r'); break; case '\\': buf.append('\\'); buf.append('\\'); break; case '\'': buf.append('\\'); buf.append('\''); break; case '"': buf.append('\\'); buf.append('\"'); break; case '\032': buf.append('\\'); buf.append('Z'); break; default: buf.append(c); break; } } return buf.toString(); } /** * Rolls a transaction back. */ public boolean rollBack() { if (_conn == null) { return false; } if (!_inTransaction) { return false; } _inTransaction = false; try { _conn.rollback(); _conn.setAutoCommit(true); return true; } catch (SQLException e) { _error.error(e); return false; } } public boolean setAttribute(int attribute, Value value) { return setAttribute(attribute, value, false); } private boolean setAttribute(int attribute, Value value, boolean isInit) { switch (attribute) { case ATTR_AUTOCOMMIT: return setAutocommit(value.toBoolean()); case ATTR_ERRMODE: return _error.setErrmode(value.toInt()); case ATTR_CASE: return setCase(value.toInt()); case ATTR_ORACLE_NULLS: return setOracleNulls(value.toInt()); case ATTR_STRINGIFY_FETCHES: return setStringifyFetches(value.toBoolean()); case ATTR_STATEMENT_CLASS: return setStatementClass(value); } if (isInit) { switch (attribute) { // TODO: there may be more of these case ATTR_TIMEOUT: return setTimeout(value.toInt()); case ATTR_PERSISTENT: return setPersistent(value.toBoolean()); } } // TODO: check what PHP does _error.unsupportedAttribute(attribute); return false; } /** * Sets the auto commit, if true commit every statement. * @return true on success, false on error. */ private boolean setAutocommit(boolean autoCommit) { if (_conn == null) { return false; } try { _conn.setAutoCommit(autoCommit); } catch (SQLException e) { _error.error(e); return false; } return true; } /** * Force column names to a specific case. * * <dl> * <dt>{@link CASE_LOWER} * <dt>{@link CASE_NATURAL} * <dt>{@link CASE_UPPER} * </dl> */ private boolean setCase(int value) { switch (value) { case CASE_LOWER: case CASE_NATURAL: case CASE_UPPER: throw new UnimplementedException(); default: _error.unsupportedAttributeValue(value); return false; } } /** * Sets whether or not the convert nulls and empty strings, works for * all drivers. * * <dl> * <dt> {@link NULL_NATURAL} * <dd> no conversion * <dt> {@link NULL_EMPTY_STRING} * <dd> empty string is converted to NULL * <dt> {@link NULL_TO_STRING} NULL * <dd> is converted to an empty string. * </dl> * * @return true on success, false on error. */ private boolean setOracleNulls(int value) { switch (value) { case NULL_NATURAL: case NULL_EMPTY_STRING: case NULL_TO_STRING: throw new UnimplementedException(); default: _error.warning(L.l("unknown value `{0}'", value)); _error.unsupportedAttributeValue(value); return false; } } private boolean setPersistent(boolean isPersistent) { return true; } private boolean setPrefetch(int prefetch) { throw new UnimplementedException(); } /** * Sets a custom statement class derived from PDOStatement. * * @param value an array(classname, array(constructor args)). * * @return true on success, false on error. */ private boolean setStatementClass(Value value) { throw new UnimplementedException("ATTR_STATEMENT_CLASS"); } /** * Convert numeric values to strings when fetching. * * @return true on success, false on error. */ private boolean setStringifyFetches(boolean stringifyFetches) { throw new UnimplementedException(); } private boolean setTimeout(int timeoutSeconds) { throw new UnimplementedException(); } /** * Opens a connection based on the dsn. */ private DataSource getDataSource(Env env, String dsn) throws Exception { if (dsn.startsWith("mysql:")) { return getMysqlDataSource(env, dsn); } if (dsn.startsWith("pgsql:")) { return getPgsqlDataSource(env, dsn); } else if (dsn.startsWith("java")) { return getJndiDataSource(env, dsn); } else if (dsn.startsWith("jdbc:")) { return getJdbcDataSource(env, dsn); } else if (dsn.indexOf(":jdbc:") > 0) { return getJdbcDataSource(env, dsn); } else if (dsn.startsWith("resin:")) { return getResinDataSource(env, dsn); } else { env.error(L.l("'{0}' is an unknown PDO data source.", dsn)); return null; } } /** * Opens a jdbc connection based on the dsn. * you can also use "driver-class:jdbc-url" */ private DataSource getJdbcDataSource(Env env, String dsn) throws Exception { if (dsn.indexOf(":jdbc:") > 0) { return env.getDataSource(dsn.substring(0, dsn.indexOf(":jdbc:")), dsn.substring(dsn.indexOf(":jdbc:") + 1)); } int t_start = dsn.indexOf(':') + 1; int t_end = dsn.indexOf(':', t_start); String tag = dsn.substring(t_start, t_end); if (driverList.containsKey(tag)) { return env.getDataSource(driverList.getProperty(tag), dsn); } return env.getDataSource(null, dsn); } /** * Opens a mysql connection based on the dsn. */ private DataSource getMysqlDataSource(Env env, String dsn) throws Exception { HashMap<String, String> attr = parseAttr(dsn, dsn.indexOf(':')); // TODO: more robust to get attribute values as is done in getPgsqlDataSource String host = attr.get("host"); String portStr = attr.get("port"); String dbname = attr.get("dbname"); String unixSocket = attr.get("unix_socket"); if (unixSocket != null) { env.error(L.l("PDO mysql: does not support unix_socket")); return null; } if (host == null) { host = "localhost"; } int port = 3306; if (portStr != null) { try { port = Integer.parseInt(portStr); } catch (NumberFormatException e) { log.log(Level.FINE, e.toString(), e); } } if (dbname == null) { dbname = "test"; } // PHP doc does not sure user and password as attributes for mysql, // but in the pgsql doc the dsn specified user and // password override arguments // passed to constructor String user = attr.get("user"); if (user != null) { _user = user; } String password = attr.get("password"); if (password != null) { _password = password; } String driver = Mysqli.DRIVER; // TODO: mysql options? String url = Mysqli.getUrl(host, port, dbname, ENCODING, false, false, false); return env.getDataSource(driver, url); } /** * Opens a postgres connection based on the dsn. */ private DataSource getPgsqlDataSource(Env env, String dsn) throws Exception { HashMap<String, String> attr = parseAttr(dsn, dsn.indexOf(':')); String host = "localhost"; String port = null; String dbname = "test"; String user = null; String password = null; for (Map.Entry<String, String> entry : attr.entrySet()) { String key = entry.getKey(); if ("host".equals(key)) { host = entry.getValue(); } else if ("port".equals(key)) { port = entry.getValue(); } else if ("dbname".equals(key)) { dbname = entry.getValue(); } else if ("user".equals(key)) { user = entry.getValue(); } else if ("password".equals(key)) { password = entry.getValue(); } else { env.warning(L.l("unknown pgsql attribute '{0}'", key)); } } // confirmed with PHP doc, the dsn specified user and password override // arguments passed to constructor if (user != null) { _user = user; } if (password != null) { _password = password; } String driver = "org.postgresql.Driver"; StringBuilder url = new StringBuilder(); url.append("jdbc:postgresql://"); url.append(host); if (port != null) { url.append(':'); url.append(port); } url.append('/'); url.append(dbname); return env.getDataSource(driver, url.toString()); } /** * Opens a resin connection based on the dsn. */ private DataSource getResinDataSource(Env env, String dsn) throws Exception { String driver = "com.caucho.db.jdbc.ConnectionPoolDataSourceImpl"; String url = "jdbc:" + dsn; return env.getDataSource(driver, url); } /** * Opens a connection based on the dsn. */ private DataSource getJndiDataSource(Env env, String dsn) { DataSource ds = null; try { Context ic = new InitialContext(); ds = (DataSource) ic.lookup(dsn); } catch (NamingException e) { log.log(Level.FINE, e.toString(), e); } if (ds == null) { env.error(L.l("'{0}' is an unknown PDO JNDI data source.", dsn)); } return ds; } private HashMap<String, String> parseAttr(String dsn, int i) { HashMap<String, String> attr = new LinkedHashMap<String, String>(); int length = dsn.length(); for (; i < length; i++) { char ch = dsn.charAt(i); if (!Character.isJavaIdentifierStart(ch)) { continue; } StringBuilder name = new StringBuilder(); for (; i < length && Character.isJavaIdentifierPart((ch = dsn.charAt(i))); i++) { name.append(ch); } for (; i < length && ((ch = dsn.charAt(i)) == ' ' || ch == '='); i++) { } StringBuilder value = new StringBuilder(); for (; i < length && (ch = dsn.charAt(i)) != ' ' && ch != ';'; i++) { value.append(ch); } attr.put(name.toString(), value.toString()); } return attr; } @Override public String toString() { // do not show password! if (_dsn == null) { return "PDO[]"; } if (_dsn.indexOf("pass") < 0) { return "PDO[" + _dsn + "]"; } int i = _dsn.lastIndexOf(':'); if (i < 0) { return "PDO[]"; } if (_dsn.startsWith("java")) { return "PDO[" + _dsn + "]"; } StringBuilder str = new StringBuilder(); str.append("PDO["); str.append(_dsn, 0, i + 1); HashMap<String, String> attr = parseAttr(_dsn, i); if (_user != null && !"".equals(_user)) { attr.put("user", _user); } boolean first = true; for (Map.Entry<String, String> entry : attr.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); if ("password".equalsIgnoreCase(key)) { value = "XXXXX"; } else if ("passwd".equalsIgnoreCase(key)) { value = "XXXXX"; } else if ("pass".equalsIgnoreCase(key)) { value = "XXXXX"; } if (!first) { str.append(' '); } first = false; str.append(key); str.append("="); str.append(value); } str.append("]"); return str.toString(); } }