/*
* 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.env.BooleanValue;
import com.caucho.quercus.env.ConnectionEntry;
import com.caucho.quercus.env.Env;
import com.caucho.quercus.env.EnvCleanup;
import com.caucho.quercus.env.StringValue;
import com.caucho.quercus.env.Value;
import com.caucho.util.L10N;
import com.caucho.util.LruCache;
import com.caucho.util.JdbcUtil;
import java.sql.*;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Represents a JDBC Connection value.
*/
public abstract class JdbcConnectionResource
implements EnvCleanup {
private static final L10N L = new L10N(JdbcConnectionResource.class);
private static final Logger log = Logger.getLogger(JdbcConnectionResource.class.getName());
private static LruCache<TableKey, JdbcTableMetaData> _tableMetadataMap = new LruCache<TableKey, JdbcTableMetaData>(256);
protected ConnectionEntry _conn;
// cached statement
private Statement _savedStmt;
private Statement _freeStmt;
private DatabaseMetaData _dmd;
private JdbcResultResource _rs;
private int _affectedRows;
private String _errorMessage = null;
private int _errorCode;
private SQLWarning _warnings;
private Env _env;
protected String _host;
protected int _port;
private String _userName;
private String _password;
protected String _driver;
protected String _url;
protected int _flags;
protected String _socket;
private String _catalog;
private boolean _isCatalogOptimEnabled = false;
private boolean _isUsed;
protected SqlParseToken _sqlParseToken = new SqlParseToken();
protected static String ENCODING;
public JdbcConnectionResource(Env env) {
_env = env;
ENCODING = env.getQuercus().getJdbcEncoding();
env.addCleanup(this);
}
/**
* Returns the error string for the most recent function call.
* This method is not invoked from PHP code.
*/
public StringValue error(Env env) {
if (isConnected()) {
return env.createString(getErrorMessage());
} else {
return env.getEmptyString();
}
}
public boolean isConnected() {
return _conn != null;
}
public Env getEnv() {
return _env;
}
public String getHost() {
return _host;
}
public String getUserName() {
return _userName;
}
public String getPassword() {
return _password;
}
public String getDbName() {
return _catalog;
}
public int getPort() {
return _port;
}
public String getDriver() {
return _driver;
}
public String getUrl() {
return _url;
}
/**
* Set the current underlying connection and
* corresponding information: host, port and
* database name.
*
* @param host server host
* @param port server port
* @param dbname database name
*/
final protected boolean connectInternal(Env env,
String host,
String userName,
String password,
String dbname,
int port,
String socket,
int flags,
String driver,
String url,
boolean isNewLink) {
if (_conn != null) {
throw new IllegalStateException(
getClass().getSimpleName() + " attempt to open multiple connections");
}
_host = host;
_userName = userName;
_password = password;
_port = port;
_socket = socket;
_flags = flags;
_driver = driver;
_url = url;
if (dbname == null) {
dbname = "";
}
_catalog = dbname;
_conn = connectImpl(env, host, userName, password,
dbname, port, socket, flags, driver, url,
isNewLink);
if (_conn != null) {
try {
if ("".equals(_catalog)) {
_catalog = _conn.getConnection().getCatalog();
}
} catch (SQLException e) {
log.log(Level.FINE, e.toString(), e);
}
}
return _conn != null && _conn.getConnection() != null;
}
/**
* Connects to the underlying database.
*/
protected abstract ConnectionEntry connectImpl(Env env,
String host,
String userName,
String password,
String dbname,
int port,
String socket,
int flags,
String driver,
String url,
boolean isNewLink);
/**
* Escape the given string for SQL statements.
*
* @param str a string
* @return the string escaped for SQL statements
*/
protected StringValue realEscapeString(StringValue str) {
StringValue buf = _env.createUnicodeBuilder();
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(0);
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;
}
/**
* Returns the affected rows from the last query.
*/
public int getAffectedRows() {
return _affectedRows;
}
public void setAffectedRows(int i) {
_affectedRows = i;
}
/**
* @return _fieldCount
*/
public int getFieldCount() {
if (_rs == null) {
return 0;
} else {
return _rs.getFieldCount();
}
}
/**
* Returns JdbcResultResource of available databases
*/
protected JdbcResultResource getCatalogs() {
clearErrors();
try {
if (_dmd == null) {
_dmd = _conn.getConnection().getMetaData();
}
ResultSet rs = _dmd.getCatalogs();
if (rs != null) {
return createResult(_env, _savedStmt, rs);
} else {
return null;
}
} catch (SQLException e) {
saveErrors(e);
log.log(Level.FINEST, e.toString(), e);
return null;
}
}
/**
* @return current catalog or false if error
*/
protected Value getCatalog() {
return _env.createString(_catalog);
}
/**
* Returns the client encoding.
*/
public String getCharacterSetName() {
if (ENCODING.equals("ISO8859_1")) {
return "latin1";
}
return ENCODING;
}
/**
* Alias for getCharacterSetName
*/
public String getClientEncoding() {
return getCharacterSetName();
}
/**
* Set encoding on the client side of the connection.
* Return true if the encoding was set, otherwise false.
*/
public boolean setClientEncoding(String encoding) {
return true;
}
/**
* Returns the client version
* @deprecated
*/
public String getClientInfo() {
try {
if (_dmd == null) {
_dmd = _conn.getConnection().getMetaData();
}
return _dmd.getDatabaseProductVersion();
} catch (SQLException e) {
log.log(Level.FINE, e.toString(), e);
return null;
}
}
/**
* Returns the connection
*/
public final Connection getConnection(Env env) {
_isUsed = true;
Connection conn = null;
if (_conn != null) {
conn = _conn.getConnection();
}
if (conn != null) {
return conn;
} else if (_errorMessage != null) {
env.warning(_errorMessage);
return null;
} else {
env.warning(L.l("Connection is not available: {0}", _conn));
_errorMessage = L.l("Connection is not available: {0}", _conn);
return null;
}
}
/**
* Returns the unwrapped SQL connection
* associated to this statement.
*/
protected Connection getJavaConnection()
throws SQLException {
// TODO: jdbc for jdk 1.6 updates
return _env.getQuercus().getConnection(_conn.getConnection());
}
/**
* Returns the data source.
*/
public String getURL() {
// return getJavaConnection().getURL();
return _url;
}
/**
* Returns the last error code.
*/
public int getErrorCode() {
return _errorCode;
}
/**
* Returns the last error message.
*/
public String getErrorMessage() {
return _errorMessage;
}
/**
*
* returns the URL string for the given connection
* IE: jdbc:mysql://localhost:3306/test
* XXX: PHP returns Localhost via UNIX socket
*/
public String getHostInfo()
throws SQLException {
if (_dmd == null) {
_dmd = _conn.getConnection().getMetaData();
}
return _dmd.getURL();
}
/**
* returns the server version
*/
public String getServerInfo()
throws SQLException {
return getMetaData().getDatabaseProductVersion();
}
/**
* Returns the table metadata.
*/
public JdbcTableMetaData getTableMetaData(Env env,
String catalog,
String schema,
String table)
throws SQLException {
try {
if (table == null || table.equals("")) {
return null;
}
TableKey key = new TableKey(getURL(), catalog, schema, table);
// TODO: needs invalidation on DROP or ALTER
JdbcTableMetaData tableMd = _tableMetadataMap.get(key);
if (tableMd != null && tableMd.isValid(env)) {
return tableMd;
}
tableMd = new JdbcTableMetaData(env,
catalog,
schema,
table,
getMetaData());
_tableMetadataMap.put(key, tableMd);
return tableMd;
} catch (SQLException e) {
log.log(Level.FINE, e.toString(), e);
return null;
}
}
private DatabaseMetaData getMetaData()
throws SQLException {
if (_dmd == null) {
_dmd = _conn.getConnection().getMetaData();
}
return _dmd;
}
static int infoToVersion(String info) {
String[] result = info.split("[.a-z-]");
if (result.length < 3) {
return 0;
}
return (Integer.parseInt(result[0]) * 10000
+ Integer.parseInt(result[1]) * 100
+ Integer.parseInt(result[2]));
}
public void closeStatement(Statement stmt) {
if (stmt == null) {
return;
}
if (_freeStmt == null && false) {
_freeStmt = stmt;
} else {
JdbcUtil.close(stmt);
}
}
/**
* Closes the connection.
*/
public boolean close(Env env) {
// php/1418
// cleanup();
ConnectionEntry conn = _conn;
_conn = null;
if (conn != null) {
conn.phpClose();
}
return true;
}
/**
* Implements the EnvCleanup interface. This method
* will deallocate resources associated with this
* connection. This method can be invoked via a
* call to close(), or it can be invoked when the
* environment is being cleaned up after a quercus
* request has been processed.
*/
@Override
public void cleanup() {
if (log.isLoggable(Level.FINER)) {
log.finer(this + " cleanup()");
}
try {
Statement savedStmt = _savedStmt;
_savedStmt = null;
Statement freeStmt = _freeStmt;
_freeStmt = null;
if (savedStmt != null) {
savedStmt.close();
}
if (freeStmt != null) {
freeStmt.close();
}
} catch (Throwable e) {
// must catch throwable to force the conn close to work
log.log(Level.FINER, e.toString(), e);
}
ConnectionEntry conn = _conn;
_conn = null;
if (conn != null) {
conn.phpClose();
}
}
public JdbcConnectionResource validateConnection() {
if (_conn == null) {
throw _env.createErrorException(
L.l("Connection is not properly initialized {0}\nDriver {1}",
_url, _driver));
}
return this;
}
/**
* Execute a single query.
*/
protected Value realQuery(Env env, String sql) {
clearErrors();
_rs = null;
Statement stmt = _freeStmt;
_freeStmt = null;
try {
Connection conn = getConnection(env);
if (conn == null) {
return BooleanValue.FALSE;
}
if (checkSql(_conn, sql)) {
return BooleanValue.TRUE;
}
// statement reuse does not gain performance significantly (< 1%)
// php/142v
if (true || stmt == null) {
// TODO: test for performance
boolean isSeekable = isSeekable();
if (isSeekable) {
stmt = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_READ_ONLY);
} else {
stmt = conn.createStatement();
}
stmt.setEscapeProcessing(false); // php/1406
}
if (stmt.execute(sql)) {
// Statement.execute(String) returns true when SQL statement is a
// SELECT statement that returns a result set.
ResultSet rs = stmt.getResultSet();
_rs = createResult(_env, stmt, rs);
_affectedRows = 0;
// TODO: if these are needed, get them lazily for performance
// _warnings = stmt.getWarnings();
} else {
// Statement.execute(String) returns false when SQL statement does
// not returns a result set (UPDATE, INSERT, DELETE, or REPLACE).
// php/430a should return a result set
// for update statements. It is always
// null though. So keep the stmt for
// future reference (PostgresModule.pg_last_oid)
// php/1f33
// This is overriden in Postgres.java
keepResourceValues(stmt);
_affectedRows = 0;
_affectedRows = stmt.getUpdateCount();
if (_rs != null) {
_rs.setAffectedRows(_affectedRows);
}
// TODO: if these are needed, get them lazily for performance
// _warnings = stmt.getWarnings();
// for php/430a
if (keepStatementOpen()) {
_savedStmt = stmt;
} else {
// _warnings = stmt.getWarnings();
_freeStmt = stmt;
}
}
} catch (DataTruncation truncationError) {
try {
_affectedRows = stmt.getUpdateCount();
//_warnings = stmt.getWarnings();
} catch (SQLException e) {
saveErrors(e);
log.log(Level.FINEST, e.toString(), e);
return BooleanValue.FALSE;
}
} catch (SQLException e) {
saveErrors(e);
// php/431h
if (keepStatementOpen()) {
keepResourceValues(stmt);
} else {
log.log(Level.FINEST, e.toString(), e);
return BooleanValue.FALSE;
}
} catch (IllegalStateException e) {
// #2184, some drivers return this on closed connection
saveErrors(new SQLExceptionWrapper(e));
return BooleanValue.FALSE;
}
if (_rs == null) {
return BooleanValue.TRUE;
}
return env.wrapJava(_rs);
}
private boolean checkSql(ConnectionEntry connEntry, String sql) {
SqlParseToken tok = parseSqlToken(sql, null);
if (tok == null) {
return false;
}
switch (tok.getFirstChar()) {
case 'a':
case 'A': {
// drop/alter clears metadata cache
_tableMetadataMap.clear();
break;
}
case 'd':
case 'D': {
if (tok.matchesToken("DROP")) {
// drop/alter clears metadata cache
_tableMetadataMap.clear();
// If DROP is dropping the current database, then clear
// the cached database name in the driver.
//
// php/144a
tok = parseSqlToken(sql, tok);
if ((tok != null) && tok.matchesToken("DATABASE")) {
tok = parseSqlToken(sql, tok);
if (tok != null) {
String dbname = tok.toUnquotedString();
if (dbname.equals(_catalog)) {
try {
setCatalog(null);
} catch (SQLException e) {
log.log(Level.FINEST, e.toString(), e);
}
}
}
}
}
break;
}
case 'c':
case 'C': {
if (tok.matchesToken("CREATE")) {
// don't pool connections that create tables, because of mysql
// temporary tables
connEntry.markForPoolRemoval();
}
/*
else if (tok.matchesToken("COMMIT")) {
commit();
setAutoCommit(true);
return true;
}
*/
break;
}
// reason for comment out? no real perf gain?
/*
case 'b': case 'B': {
if (tok.matchesToken("BEGIN")) {
// Test for mediawiki performance
setAutoCommit(false);
return true;
}
break;
}
case 'r': case 'R': {
if (tok.matchesToken("ROLLBACK")) {
rollback();
setAutoCommit(true);
return true;
}
break;
}
*/
}
return false;
}
/**
* Parse a token from a string containing a SQL statement.
* If the prevToken is null, then the first token in parsed.
* If a SQL token can't be found in the string, then null
* is returned. If a SQL token is found, data is captured in
* the returned SqlParseToken result.
*/
protected SqlParseToken parseSqlToken(String sql, SqlParseToken prevToken) {
if (sql == null) {
_sqlParseToken.init();
return null;
}
final int len = sql.length();
int i, start;
// Start at index 0, or where we left off last time
if (prevToken == null) {
i = 0;
} else {
i = prevToken._end;
}
while (i < len && Character.isWhitespace(sql.charAt(i))) {
i++;
}
// Must be at least 1 non-whitespace character
if ((i + 1) >= len) {
_sqlParseToken.init();
return null;
}
start = i;
while (i < len && !Character.isWhitespace(sql.charAt(i))) {
i++;
}
_sqlParseToken.assign(sql, start, i);
return _sqlParseToken;
}
/**
* Creates a database-specific result.
*/
protected JdbcResultResource createResult(Env env,
Statement stmt,
ResultSet rs) {
return new JdbcResultResource(env, stmt, rs, this);
}
/**
* sets auto-commmit to true or false
*/
public boolean setAutoCommit(boolean mode) {
clearErrors();
try {
_conn.getConnection().setAutoCommit(mode);
} catch (SQLException e) {
saveErrors(e);
log.log(Level.FINEST, e.toString(), e);
return false;
}
return true;
}
/**
* commits the transaction of the current connection
*/
public boolean commit() {
clearErrors();
try {
_conn.getConnection().commit();
} catch (SQLException e) {
saveErrors(e);
log.log(Level.FINEST, e.toString(), e);
return false;
}
return true;
}
/**
* rolls the current transaction back
*
* NOTE: quercus doesn't seem to support the idea
* of savepoints
*/
public boolean rollback() {
clearErrors();
try {
_conn.getConnection().rollback();
} catch (SQLException e) {
saveErrors(e);
log.log(Level.FINEST, e.toString(), e);
return false;
}
return true;
}
/**
* Sets the catalog
*/
public void setCatalog(String name)
throws SQLException {
if (_catalog != null && _catalog.equals(name)) {
return;
}
clearErrors();
// php/142v
// mysql jdbc: can't reuse old statements after a USE query
_savedStmt = null;
_freeStmt = null;
if (!_isUsed && _isCatalogOptimEnabled) {
// The database is only connected, but not used, reopen with
// a real catalog
ConnectionEntry conn = _conn;
_conn = null;
if (conn != null) {
conn.phpClose();
}
connectInternal(_env, _host, _userName, _password, name,
_port, _socket, _flags, _driver, _url, false);
} else {
_conn.setCatalog(name);
}
_catalog = name;
}
/**
* Converts to an object.
*/
public Object toObject() {
return null;
}
/**
* Converts to a string.
*/
@Override
public String toString() {
if (_conn != null) {
return getClass().getSimpleName() + "[" + _conn.getConnection() + "]";
} else {
return getClass().getSimpleName() + "[" + null + "]";
}
}
/**
* This function is overriden in Postgres to keep
* result set references for php/430a (see also php/1f33)
*/
protected void keepResourceValues(Statement stmt) {
return;
}
/**
* This function is overriden in Postgres to keep
* statement references for php/430a
*/
protected boolean keepStatementOpen() {
return false;
}
/**
* Get the current result resource
*/
protected JdbcResultResource getResultResource() {
return _rs;
}
/**
* Set the current result resource
*/
protected void setResultResource(JdbcResultResource rs) {
_rs = rs;
}
/**
* This function was added for PostgreSQL pg_last_notice
*
* @return warning messages
*/
protected SQLWarning getWarnings() {
return _warnings;
}
/**
* Pings the database
*/
public boolean ping(Env env) {
try {
return isConnected() && !getConnection(env).isClosed();
} catch (SQLException e) {
log.log(Level.FINE, e.toString(), e);
env.warning(e.toString(), e);
return false;
}
}
/**
* Set the current SQL warnings.
*
* @param warnings the new SQL warnings
*/
protected void setWarnings(SQLWarning warnings) {
_warnings = warnings;
}
protected void clearErrors() {
_errorMessage = null;
_errorCode = 0;
_warnings = null;
}
protected void saveErrors(SQLException e) {
_errorMessage = e.getMessage();
if (_errorMessage == null || "".equals(_errorMessage)) {
_errorMessage = e.toString();
}
_errorCode = e.getErrorCode();
}
/**
* Returns true if this connection supports TYPE_SCROLL_INSENSITIVE.
* http://bugs.caucho.com/view.php?id=3746
*/
protected boolean isSeekable() {
return true;
}
static class TableKey {
private final String _url;
private final String _catalog;
private final String _schema;
private final String _table;
TableKey(String url, String catalog, String schema, String table) {
_url = url;
_catalog = catalog;
_schema = schema;
_table = table;
}
@Override
public int hashCode() {
int hash = 37;
if (_url != null) {
hash = 65537 * hash + _url.hashCode();
}
if (_catalog != null) {
hash = 65537 * hash + _catalog.hashCode();
}
if (_schema != null) {
hash = 65537 * hash + _schema.hashCode();
}
if (_table != null) {
hash = 65537 * hash + _table.hashCode();
}
return hash;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
} else if (!(o instanceof TableKey)) {
return false;
}
TableKey key = (TableKey) o;
if ((_url == null) != (key._url == null)) {
return false;
} else if (_url != null && !_url.equals(key._url)) {
return false;
}
if ((_catalog == null) != (key._catalog == null)) {
return false;
} else if (_catalog != null && !_catalog.equals(key._catalog)) {
return false;
}
if ((_schema == null) != (key._schema == null)) {
return false;
} else if (_schema != null && !_schema.equals(key._schema)) {
return false;
}
if ((_table == null) != (key._table == null)) {
return false;
} else if (_table != null && !_table.equals(key._table)) {
return false;
}
return true;
}
}
/*
* This class enables efficient parsing of a SQL token from
* a String. An SQL statement can be parsed one token at a
* time. One can efficiently check that first letter of
* the parse token via matchesFirstChar() without creating
* a substring from the original.
*/
protected static class SqlParseToken {
private String _query;
private String _token;
private int _start;
private int _end;
private char _firstChar;
public void init() {
_query = null;
_token = null;
_start = -1;
_end = -1;
_firstChar = '\0';
}
public void assign(String query, int start, int end) {
_query = query;
_token = null;
_start = start;
_end = end;
_firstChar = query.charAt(start);
}
public boolean matchesFirstChar(char upper, char lower) {
return (_firstChar == upper) || (_firstChar == lower);
}
public char getFirstChar() {
return _firstChar;
}
// Case insensitive compare of token string
public boolean matchesToken(String token) {
if (_token == null) {
_token = _query.substring(_start, _end);
}
return _token.equalsIgnoreCase(token);
}
@Override
public String toString() {
if (_token == null) {
_token = _query.substring(_start, _end);
}
return _token;
}
/*
* Return the SQL token as a string. If the token
* is back quoted, then remove the back quotes and
* return the string inside.
*/
public String toUnquotedString() {
String tok = toString();
// Extract database name if back quoted : "DROP DATABASE `DBNAME`"
if (tok.length() >= 2
&& tok.charAt(0) == '`'
&& tok.charAt(tok.length() - 1) == '`') {
tok = tok.substring(1, tok.length() - 1);
}
return tok;
}
}
}