/******************************************************************************* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. *******************************************************************************/ package org.apache.ofbiz.entity.jdbc; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.math.BigDecimal; import java.sql.Blob; import java.sql.Clob; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; import java.sql.Types; import java.util.ArrayList; import java.util.List; import org.apache.ofbiz.base.util.Debug; import org.apache.ofbiz.entity.Delegator; import org.apache.ofbiz.entity.GenericDataSourceException; import org.apache.ofbiz.entity.GenericEntityException; import org.apache.ofbiz.entity.config.model.Datasource; import org.apache.ofbiz.entity.config.model.EntityConfig; import org.apache.ofbiz.entity.datasource.GenericHelperInfo; import org.apache.ofbiz.entity.transaction.GenericTransactionException; import org.apache.ofbiz.entity.transaction.TransactionFactoryLoader; import org.apache.ofbiz.entity.transaction.TransactionUtil; /** * SQLProcessor - provides utility functions to ease database access * */ public class SQLProcessor { /** Module Name Used for debugging */ public static final String module = SQLProcessor.class.getName(); /** Used for testing connections when test is enabled */ public static List<String> CONNECTION_TEST_LIST = new ArrayList<String>(); public static int MAX_CONNECTIONS = 1000; public static boolean ENABLE_TEST = false; private final Delegator delegator; /** The datasource helper (see entityengine.xml <datasource name="..">) */ private GenericHelperInfo helperInfo; // / The database resources to be used private Connection _connection = null; // / The database resources to be used private PreparedStatement _ps = null; // / The database resources to be used private ResultSet _rs = null; private ResultSetMetaData _rsmd = null; // / The SQL String used. Use for debugging only private String _sql; // / Index to be used with preparedStatement.setValue(_ind, ...) private int _ind; // / true in case of manual transactions private boolean _manualTX; // / true in case the connection shall be closed. private boolean _bDeleteConnection = false; /** * Construct an object based on the helper/datasource * * @param helperInfo The datasource helper (see entityengine.xml <datasource name="..">) */ public SQLProcessor(Delegator delegator, GenericHelperInfo helperInfo) { this.delegator = delegator; this.helperInfo = helperInfo; this._manualTX = true; } /** * Construct an object with an connection given. The connection will not * be closed by this SQLProcessor, but may be by some other. * * @param helperInfo The datasource helper (see entityengine.xml <datasource name="..">) * @param connection The connection to be used */ public SQLProcessor(Delegator delegator, GenericHelperInfo helperInfo, Connection connection) { this.delegator = delegator; this.helperInfo = helperInfo; this._connection = connection; // Do not commit while closing if (_connection != null) { _manualTX = false; } } public Delegator getDelegator() { return delegator; } ResultSetMetaData getResultSetMetaData() { if (_rsmd == null) { // try the ResultSet, if not null, or try the PreparedStatement, also if not null try { if (_rs != null) { _rsmd = _rs.getMetaData(); } else if (_ps != null) { _rsmd = _ps.getMetaData(); } } catch (SQLException sqle2) { Debug.logWarning("[SQLProcessor.rollback]: SQL Exception while rolling back insert. Error was:" + sqle2, module); Debug.logWarning(sqle2, module); } } return _rsmd; } /** * Commit all modifications * * @throws GenericDataSourceException */ public void commit() throws GenericDataSourceException { if (_connection == null) { return; } if (Debug.verboseOn()) Debug.logVerbose("SQLProcessor:commit() _manualTX=" + _manualTX, module); if (_manualTX) { try { _connection.commit(); if (Debug.verboseOn()) Debug.logVerbose("SQLProcessor:commit() : called commit on connection", module); } catch (SQLException sqle) { Debug.logError(sqle, "Error committing transaction: " + sqle.toString()); try { rollback(); } catch (GenericDataSourceException rbsqle) { Debug.logError(rbsqle, "Got another error when trying to rollback after error committing transaction: " + sqle.toString()); } throw new GenericDataSourceException("SQL Exception occurred on commit", sqle); } } } /** * Rollback all modifications */ public void rollback() throws GenericDataSourceException { if (_connection == null) { return; } if (Debug.verboseOn()) Debug.logVerbose("SQLProcessor:rollback() _manualTX=" + _manualTX, module); try { if (_manualTX) { _connection.rollback(); if (Debug.verboseOn()) Debug.logVerbose("SQLProcessor:rollback() : _manualTX=" + _manualTX, module); } else { try { TransactionUtil.setRollbackOnly("rollback called in Entity Engine SQLProcessor", new Exception("Current Location Stack")); if (Debug.verboseOn()) Debug.logVerbose("SQLProcessor:rollback() : _manualTX=" + _manualTX, module); } catch (GenericTransactionException e) { Debug.logError(e, "Error setting rollback only", module); throw new GenericDataSourceException("Error setting rollback only", e); } } } catch (SQLException sqle2) { Debug.logWarning("[SQLProcessor.rollback]: SQL Exception while rolling back insert. Error was:" + sqle2, module); Debug.logWarning(sqle2, module); } } /** * Commit if required and remove all allocated resources * * @throws GenericDataSourceException */ public void close() throws GenericDataSourceException { if (_manualTX) { if (Debug.verboseOn()) Debug.logVerbose("SQLProcessor:close() calling commit : _manualTX=" + _manualTX, module); commit(); } _sql = null; if (_rs != null) { try { _rs.close(); if (Debug.verboseOn()) Debug.logVerbose("SQLProcessor:close() result close : _manualTX=" + _manualTX, module); } catch (SQLException sqle) { Debug.logWarning(sqle.getMessage(), module); } _rs = null; } if (_ps != null) { try { _ps.close(); if (Debug.verboseOn()) Debug.logVerbose("SQLProcessor:close() preparedStatement close : _manualTX=" + _manualTX, module); } catch (SQLException sqle) { Debug.logWarning(sqle.getMessage(), module); } _ps = null; } if ((_connection != null) && _bDeleteConnection) { try { _connection.close(); if (Debug.verboseOn()) Debug.logVerbose("SQLProcessor:close() connection close : _manualTX=" + _manualTX, module); } catch (SQLException sqle) { Debug.logWarning(sqle.getMessage(), module); } _connection = null; } } /** * Get a connection from the TransactionFactoryLoader * * @return The connection created * * @throws GenericDataSourceException * @throws GenericEntityException */ public Connection getConnection() throws GenericDataSourceException, GenericEntityException { if (_connection != null) return _connection; _manualTX = true; try { _connection = TransactionFactoryLoader.getInstance().getConnection(helperInfo); if (Debug.verboseOn()) Debug.logVerbose("SQLProcessor:connection() : manualTx=" + _manualTX, module); } catch (SQLException sqle) { throw new GenericDataSourceException("Unable to establish a connection with the database.", sqle); } // make sure we actually did get a connection if (_connection == null) { throw new GenericDataSourceException("Unable to establish a connection with the database. Connection was null!"); } // test the connection testConnection(_connection); /* causes problems w/ postgres ?? if (Debug.verboseOn()) { int isoLevel = -999; try { isoLevel = _connection.getTransactionIsolation(); } catch (SQLException e) { Debug.logError(e, "Problems getting the connection's isolation level", module); } if (isoLevel == Connection.TRANSACTION_NONE) { Debug.logVerbose("Transaction isolation level set to 'None'.", module); } else if (isoLevel == Connection.TRANSACTION_READ_COMMITTED) { Debug.logVerbose("Transaction isolation level set to 'ReadCommited'.", module); } else if (isoLevel == Connection.TRANSACTION_READ_UNCOMMITTED) { Debug.logVerbose("Transaction isolation level set to 'ReadUncommitted'.", module); } else if (isoLevel == Connection.TRANSACTION_REPEATABLE_READ) { Debug.logVerbose("Transaction isolation level set to 'RepeatableRead'.", module); } else if (isoLevel == Connection.TRANSACTION_SERIALIZABLE) { Debug.logVerbose("Transaction isolation level set to 'Serializable'.", module); } } */ // always try to set auto commit to false, but if we can't then later on we won't commit try { if (_connection.getAutoCommit()) { try { _connection.setAutoCommit(false); if (Debug.verboseOn()) Debug.logVerbose("SQLProcessor:setAutoCommit(false) : manualTx=" + _manualTX, module); } catch (SQLException sqle) { _manualTX = false; } } } catch (SQLException e) { throw new GenericDataSourceException("Cannot get autoCommit status from connection", e); } try { if (TransactionUtil.getStatus() == TransactionUtil.STATUS_ACTIVE) { if (Debug.verboseOn()) Debug.logVerbose("[SQLProcessor.getConnection] : active transaction", module); _manualTX = false; } } catch (GenericTransactionException e) { // nevermind, don't worry about it, but print the exc anyway Debug.logWarning("[SQLProcessor.getConnection]: Exception was thrown trying to check " + "transaction status: " + e.toString(), module); } if (Debug.verboseOn()) Debug.logVerbose("[SQLProcessor.getConnection] : con=" + _connection, module); _bDeleteConnection = true; return _connection; } /** * Prepare a statement. In case no connection has been given, allocate a * new one. * * @param sql The SQL statement to be executed * * @throws GenericDataSourceException * @throws GenericEntityException */ public void prepareStatement(String sql) throws GenericDataSourceException, GenericEntityException { this.prepareStatement(sql, false, 0, 0, -1, -1); } /** * Prepare a statement. In case no connection has been given, allocate a * new one. * * @param sql The SQL statement to be executed * * @throws GenericDataSourceException * @throws GenericEntityException */ public void prepareStatement(String sql, boolean specifyTypeAndConcur, int resultSetType, int resultSetConcurrency) throws GenericDataSourceException, GenericEntityException { this.prepareStatement(sql, specifyTypeAndConcur, resultSetType, resultSetConcurrency, -1, -1); } /** * Prepare a statement. In case no connection has been given, allocate a * new one. * * @param sql The SQL statement to be executed * * @throws GenericDataSourceException * @throws GenericEntityException */ public void prepareStatement(String sql, boolean specifyTypeAndConcur, int resultSetType, int resultSetConcurrency, int fetchSize, int maxRows) throws GenericDataSourceException, GenericEntityException { if (Debug.verboseOn()) Debug.logVerbose("[SQLProcessor.prepareStatement] sql=" + sql, module); if (_connection == null) { getConnection(); } try { _sql = sql; _ind = 1; if (specifyTypeAndConcur) { _ps = _connection.prepareStatement(sql, resultSetType, resultSetConcurrency); if (Debug.verboseOn()) Debug.logVerbose("[SQLProcessor.prepareStatement] _ps=" + _ps, module); } else { _ps = _connection.prepareStatement(sql); if (Debug.verboseOn()) Debug.logVerbose("[SQLProcessor.prepareStatement] (def) _ps=" + _ps, module); } if (maxRows > 0) { _ps.setMaxRows(maxRows); if (Debug.verboseOn()) Debug.logVerbose("[SQLProcessor.prepareStatement] max rows set : " + maxRows, module); } this.setFetchSize(_ps, fetchSize); } catch (SQLException sqle) { throw new GenericDataSourceException("SQL Exception while executing the following:" + sql, sqle); } } /** * Execute a query based on the prepared statement * * @return The result set of the query * @throws GenericDataSourceException */ public ResultSet executeQuery() throws GenericDataSourceException { try { // if (Debug.verboseOn()) Debug.logVerbose("[SQLProcessor.executeQuery] ps=" + _ps.toString(), module); _rs = _ps.executeQuery(); } catch (SQLException sqle) { this.checkLockWaitInfo(sqle); throw new GenericDataSourceException("SQL Exception while executing the following:" + _sql, sqle); } return _rs; } /** * Execute a query based on the SQL string given * * @param sql The SQL string to be executed * @return The result set of the query * @throws GenericEntityException * @throws GenericDataSourceException */ public ResultSet executeQuery(String sql) throws GenericDataSourceException, GenericEntityException { prepareStatement(sql); return executeQuery(); } /** * Execute updates * * @return The number of rows updated * @throws GenericDataSourceException */ public int executeUpdate() throws GenericDataSourceException { try { // if (Debug.verboseOn()) Debug.logVerbose("[SQLProcessor.executeUpdate] ps=" + _ps.toString(), module); //TransactionUtil.printAllThreadsTransactionBeginStacks(); return _ps.executeUpdate(); } catch (SQLException sqle) { this.checkLockWaitInfo(sqle); // don't display this here, may not be critical, allow handling further up... Debug.logError(sqle, "SQLProcessor.executeUpdate() : ERROR : ", module); throw new GenericDataSourceException("SQL Exception while executing the following:" + _sql, sqle); } } /** * Execute update based on the SQL statement given * * @param sql SQL statement to be executed * @throws GenericDataSourceException */ public int executeUpdate(String sql) throws GenericDataSourceException { Statement stmt = null; try { stmt = _connection.createStatement(); return stmt.executeUpdate(sql); } catch (SQLException sqle) { // passing on this exception as nested, no need to log it here: Debug.logError(sqle, "SQLProcessor.executeUpdate(sql) : ERROR : ", module); throw new GenericDataSourceException("SQL Exception while executing the following:" + _sql, sqle); } finally { if (stmt != null) { try { stmt.close(); } catch (SQLException sqle) { Debug.logWarning("Unable to close 'statement': " + sqle.getMessage(), module); } } } } /** * Test if there more records available * * @return true, if there more records available * * @throws GenericDataSourceException */ public boolean next() throws GenericDataSourceException { try { return _rs.next(); } catch (SQLException sqle) { throw new GenericDataSourceException("SQL Exception while executing the following:" + _sql, sqle); } } /** * Getter: get the currently active ResultSet * * @return ResultSet */ public ResultSet getResultSet() { return _rs; } /** * Getter: get the prepared statement * * @return PreparedStatement */ public PreparedStatement getPreparedStatement() { return _ps; } /** * Execute a query based on the SQL string given. For each record * of the ResultSet return, execute a callback function * * @param sql The SQL string to be executed * @param aListener The callback function object * * @throws GenericEntityException */ public void execQuery(String sql, ExecQueryCallbackFunctionIF aListener) throws GenericEntityException { if (_connection == null) { getConnection(); } try { if (Debug.verboseOn()) Debug.logVerbose("[SQLProcessor.execQuery]: " + sql, module); executeQuery(sql); // process the results by calling the listener for // each row... boolean keepGoing = true; while (keepGoing && _rs.next()) { keepGoing = aListener.processNextRow(_rs); } if (_manualTX) { _connection.commit(); } } catch (SQLException sqle) { Debug.logWarning("[SQLProcessor.execQuery]: SQL Exception while executing the following:\n" + sql + "\nError was:", module); Debug.logWarning(sqle.getMessage(), module); throw new GenericEntityException("SQL Exception while executing the following:" + _sql, sqle); } finally { close(); } } /** * Set the next binding variable of the currently active prepared statement. * * @param handler * @param field * * @throws SQLException */ public <T> void setValue(JdbcValueHandler<T> handler, T field) throws SQLException { handler.setValue(_ps, _ind, field); _ind++; } /** * Set the next binding variable of the currently active prepared statement. * * @param field * * @throws SQLException */ public void setValue(String field) throws SQLException { //ResultSetMetaData rsmd = this.getResultSetMetaData(); //this doesn't seem to work, query not yet parsed: int colType = rsmd.getColumnType(_ind); if (field != null) { //if (field.length() > 4000) { //Clob clb = new Cl // doesn't work with Oracle drivers, need the funky work-around: _ps.setCharacterStream(_ind, new StringReader(field), field.length()); //_needClobWorkAroundWrite.put(Integer.valueOf(_ind), field); //_ps.setString(_ind, " "); //} else { _ps.setString(_ind, field); //} } else { // silly workaround for Derby (Cloudscape 10 beta Bug #5928) // this should be removed after the know bug is fixed try { _ps.setNull(_ind, Types.VARCHAR); } catch (SQLException e) { try { _ps.setString(_ind, null); } catch (SQLException e2) { Debug.logError(e2, module); throw e; } } } _ind++; } /** * Set the next binding variable of the currently active prepared statement. * * @param field * * @throws SQLException */ public void setValue(java.sql.Timestamp field) throws SQLException { if (field != null) { _ps.setTimestamp(_ind, field); } else { _ps.setNull(_ind, Types.TIMESTAMP); } _ind++; } /** * Set the next binding variable of the currently active prepared statement. * * @param field * * @throws SQLException */ public void setValue(java.sql.Time field) throws SQLException { if (field != null) { _ps.setTime(_ind, field); } else { _ps.setNull(_ind, Types.TIME); } _ind++; } /** * Set the next binding variable of the currently active prepared statement. * * @param field * * @throws SQLException */ public void setValue(java.sql.Date field) throws SQLException { if (field != null) { _ps.setDate(_ind, field); } else { _ps.setNull(_ind, Types.DATE); } _ind++; } /** * Set the next binding variable of the currently active prepared statement. * * @param field * * @throws SQLException */ public void setValue(Integer field) throws SQLException { if (field != null) { _ps.setInt(_ind, field.intValue()); } else { _ps.setNull(_ind, Types.NUMERIC); } _ind++; } /** * Set the next binding variable of the currently active prepared statement. * * @param field * * @throws SQLException */ public void setValue(Long field) throws SQLException { if (field != null) { _ps.setLong(_ind, field.longValue()); } else { _ps.setNull(_ind, Types.NUMERIC); } _ind++; } /** * Set the next binding variable of the currently active prepared statement. * * @param field * * @throws SQLException */ public void setValue(Float field) throws SQLException { if (field != null) { _ps.setFloat(_ind, field.floatValue()); } else { _ps.setNull(_ind, Types.NUMERIC); } _ind++; } /** * Set the next binding variable of the currently active prepared statement. * * @param field * * @throws SQLException */ public void setValue(Double field) throws SQLException { if (field != null) { _ps.setDouble(_ind, field.doubleValue()); } else { _ps.setNull(_ind, Types.NUMERIC); } _ind++; } /** * Set the next binding variable of the currently active prepared statement. * * @param field * * @throws SQLException */ public void setValue(BigDecimal field) throws SQLException { if (field != null) { _ps.setBigDecimal(_ind, field); } else { _ps.setNull(_ind, Types.NUMERIC); } _ind++; } /** * Set the next binding variable of the currently active prepared statement. * * @param field * * @throws SQLException */ public void setValue(Boolean field) throws SQLException { if (field != null) { _ps.setBoolean(_ind, field.booleanValue()); } else { _ps.setNull(_ind, Types.BOOLEAN); } _ind++; } /** * Set the next binding variable of the currently active prepared statement. * * @param field * * @throws SQLException */ public void setValue(Object field) throws SQLException { if (field != null) { _ps.setObject(_ind, field, Types.JAVA_OBJECT); } else { _ps.setNull(_ind, Types.JAVA_OBJECT); } _ind++; } /** * Set the next binding variable of the currently active prepared statement * * @param field * * @throws SQLException */ public void setValue(Blob field) throws SQLException { if (field != null) { _ps.setBlob(_ind, field); } else { Datasource datasourceInfo = EntityConfig.getDatasource(this.helperInfo.getHelperBaseName()); if (datasourceInfo.getUseBinaryTypeForBlob()) { _ps.setNull(_ind, Types.BINARY); } else { _ps.setNull(_ind, Types.BLOB); } } _ind++; } /** * Set the next binding variable of the currently active prepared statement * * @param field * * @throws SQLException */ public void setValue(Clob field) throws SQLException { if (field != null) { _ps.setClob(_ind, field); } else { _ps.setNull(_ind, Types.CLOB); } _ind++; } /** * Set the next binding variable of the currently active prepared statement * to write the serialized data of 'field' to a BLOB. * * @param field * * @throws SQLException */ public void setBinaryStream(Object field) throws SQLException { if (field != null) { try { ByteArrayOutputStream os = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(os); oos.writeObject(field); oos.close(); byte[] buf = os.toByteArray(); os.close(); ByteArrayInputStream is = new ByteArrayInputStream(buf); _ps.setBinaryStream(_ind, is, buf.length); is.close(); } catch (IOException ex) { throw new SQLException(ex.getMessage()); } } else { Datasource datasourceInfo = EntityConfig.getDatasource(this.helperInfo.getHelperBaseName()); if (datasourceInfo.getUseBinaryTypeForBlob()) { _ps.setNull(_ind, Types.BINARY); } else { _ps.setNull(_ind, Types.BLOB); } } _ind++; } /** * Set the next binding variable of the currently active prepared statement * to write the serialized data of 'field' to a Blob with the given bytes. * * @param bytes * * @throws SQLException */ public void setBytes(byte[] bytes) throws SQLException { if (bytes != null) { _ps.setBytes(_ind, bytes); } else { Datasource datasourceInfo = EntityConfig.getDatasource(this.helperInfo.getHelperBaseName()); if (datasourceInfo.getUseBinaryTypeForBlob()) { _ps.setNull(_ind, Types.BINARY); } else { _ps.setNull(_ind, Types.BLOB); } } _ind++; } @Override protected void finalize() throws Throwable { try { this.close(); } catch (Exception e) { Debug.logError(e, "Error closing the result, connection, etc in finalize SQLProcessor", module); } super.finalize(); } protected void testConnection(Connection con) throws GenericEntityException { if (SQLProcessor.ENABLE_TEST) { if (SQLProcessor.CONNECTION_TEST_LIST.contains(con.toString())) { throw new GenericEntityException("Connection the exact same as index " + SQLProcessor.CONNECTION_TEST_LIST.indexOf(con.toString())); } SQLProcessor.CONNECTION_TEST_LIST.add(con.toString()); if (SQLProcessor.CONNECTION_TEST_LIST.size() > SQLProcessor.MAX_CONNECTIONS) { SQLProcessor.CONNECTION_TEST_LIST.remove(0); } } } protected void setFetchSize(Statement stmt, int fetchSize) throws SQLException { // do not set fetch size when using the cursor connection if (_connection instanceof CursorConnection) return; // check if the statement was called with a specific fetch size, if not grab the default from the datasource if (fetchSize < 0) { Datasource ds = EntityConfig.getDatasource(this.helperInfo.getHelperBaseName()); if (ds != null) { fetchSize = ds.getResultFetchSize(); } else { Debug.logWarning("Datasource is null, not setting fetch size!", module); } } // otherwise only set if the size is > -1 (0 is sometimes used to note ALL rows) if (fetchSize > -1) { stmt.setFetchSize(fetchSize); } } private void checkLockWaitInfo(Exception sqle) { String eMsg = sqle.getMessage(); // see if there is a lock wait timeout error, if so try to get and print more info about it // the string for Derby is "A lock could not be obtained within the time requested" // the string for MySQL is "Lock wait timeout exceeded; try restarting transaction" if (eMsg.indexOf("A lock could not be obtained within the time requested") >= 0 || eMsg.indexOf("Lock wait timeout exceeded") >= 0) { Debug.logWarning(sqle, "Lock wait timeout error found in thread [" + Thread.currentThread().getId() + "]: (" + eMsg + ") when executing the SQL [" + _sql + "]", module); TransactionUtil.printAllThreadsTransactionBeginStacks(); } } }