/* * Copyright (C) 2004-2008 Jive Software, 2017-2024 Ignite Realtime Foundation. All rights reserved. * * Licensed 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.jivesoftware.database; import java.io.File; import java.io.Reader; import java.io.StringReader; import java.io.StringWriter; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Arrays; import java.util.HashSet; import java.util.Map; import java.util.MissingResourceException; import org.jivesoftware.util.ClassUtils; import org.jivesoftware.util.JiveGlobals; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Central manager of database connections. All methods are static so that they * can be easily accessed throughout the classes in the database package.
*
* This class also provides a set of utility methods that abstract out
* operations that may not work on all databases such as setting the max number
* or rows that a query should return.
*
* @author Jive Software
* @see ConnectionProvider
*/
public class DbConnectionManager {
private static final Logger Log = LoggerFactory.getLogger(DbConnectionManager.class);
private static ConnectionProvider connectionProvider;
private static final Object providerLock = new Object();
// True if connection profiling is turned on. Always false by default.
private static boolean profilingEnabled = false;
// True if the database support transactions.
private static boolean transactionsSupported;
// True if the database requires large text fields to be streamed.
private static boolean streamTextRequired;
/** True if the database supports the Statement.setMaxRows() method. */
private static boolean maxRowsSupported;
/** True if the database supports the rs.setFetchSize() method. */
private static boolean fetchSizeSupported;
// True if the database supports correlated subqueries.
private static boolean subqueriesSupported;
// True if the database supports scroll-insensitive results.
private static boolean scrollResultsSupported;
// True if the database supports batch updates.
private static boolean batchUpdatesSupported;
/** True if the database supports the Statement.setFetchSize()) method. */
private static boolean pstmt_fetchSizeSupported = true;
/** The char used to quote identifiers */
private static String identifierQuoteString;
private static final String SETTING_DATABASE_MAX_RETRIES = "database.maxRetries";
private static final String SETTING_DATABASE_RETRY_DELAY = "database.retryDelay";
private static DatabaseType databaseType = DatabaseType.unknown;
private static SchemaManager schemaManager = new SchemaManager();
/**
* Ensures that the connection provider exists and is set
*/
private static void ensureConnectionProvider() {
if (connectionProvider != null) return;
synchronized (providerLock) {
if (connectionProvider != null) return;
// Attempt to load the connection provider classname as a Jive property.
String className = JiveGlobals.getXMLProperty("connectionProvider.className");
if (className != null) {
// Attempt to load the class.
try {
Class conClass = ClassUtils.forName(className);
setConnectionProvider((ConnectionProvider)conClass.newInstance());
} catch (Exception e) {
Log.warn("Failed to create the " +
"connection provider specified by connection" +
"Provider.className. Using the default pool.", e);
setConnectionProvider(new DefaultConnectionProvider());
}
} else {
setConnectionProvider(new DefaultConnectionProvider());
}
}
}
/**
* Attempts to create a connection to the database and execute a query.
*
* @param errors A map populated with errors if they occur.
* @return true if the test was successful, otherwise false.
*/
public static boolean testConnection( MapabortTransaction.
*
* @param pstmt the prepared statement to close.
* @param con the connection to close.
* @param abortTransaction true if the transaction should be rolled back.
*/
public static void closeTransactionConnection(PreparedStatement pstmt, Connection con,
boolean abortTransaction)
{
closeStatement(pstmt);
closeTransactionConnection(con, abortTransaction);
}
/**
* Closes a Connection. However, it first rolls back the transaction or
* commits it depending on the value of abortTransaction.
*
* @param con the connection to close.
* @param abortTransaction true if the transaction should be rolled back.
*/
public static void closeTransactionConnection(Connection con, boolean abortTransaction) {
if (con != null) {
// Rollback or commit the transaction
if (isTransactionsSupported()) {
try {
if (abortTransaction) {
con.rollback();
}
else {
con.commit();
}
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
// Reset the connection to auto-commit mode.
try {
con.setAutoCommit(true);
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
closeConnection(con);
}
}
/**
* Closes a result set. This method should be called within the finally section of
* your database logic, as in the following example:
*
*
* public void doSomething(Connection con) {
* ResultSet rs = null;
* PreparedStatement pstmt = null;
* try {
* pstmt = con.prepareStatement("select * from blah");
* rs = pstmt.executeQuery();
* ....
* }
* catch (SQLException sqle) {
* Log.error(sqle.getMessage(), sqle);
* }
* finally {
* ConnectionManager.closeResultSet(rs);
* ConnectionManager.closePreparedStatement(pstmt);
* }
* }
*
* @param rs the result set to close.
*/
public static void closeResultSet(ResultSet rs) {
if (rs != null) {
try {
rs.close();
}
catch (SQLException e) {
Log.error(e.getMessage(), e);
}
}
}
/**
* Closes a statement. This method should be called within the finally section of
* your database logic, as in the following example:
*
*
* public void doSomething(Connection con) {
* PreparedStatement pstmt = null;
* try {
* pstmt = con.prepareStatement("select * from blah");
* ....
* }
* catch (SQLException sqle) {
* Log.error(sqle.getMessage(), sqle);
* }
* finally {
* ConnectionManager.closeStatement(pstmt);
* }
* }
*
* @param stmt the statement.
*/
public static void closeStatement(Statement stmt) {
if (stmt != null) {
try {
stmt.close();
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
}
/**
* Closes a statement and a result set. This method should be called within the finally section of
* your database logic, as in the following example:
*
*
* public void doSomething(Connection con) {
* PreparedStatement pstmt = null;
* ResultSet rs = null;
* try {
* pstmt = con.prepareStatement("select * from blah");
* rs = ...
* ....
* }
* catch (SQLException sqle) {
* Log.error(sqle.getMessage(), sqle);
* }
* finally {
* ConnectionManager.closeStatement(rs, pstmt);
* }
* }
*
* @param rs the result set to close
* @param stmt the statement.
*/
public static void closeStatement(ResultSet rs, Statement stmt) {
closeResultSet(rs);
closeStatement(stmt);
}
/**
* Closes a statement. This method should be called within the try section of
* your database logic when you reuse a statement. It may throws an exception,
* so don't place it in the finally section.
* Example:
*
*
* public void doSomething(Connection con) {
* PreparedStatement pstmt = null;
* try {
* pstmt = con.prepareStatement("select * from dual");
* pstmt.executeUpdate();
* ...
* ConnectionManager.fastcloseStmt(pstmt);
* pstmt = con.prepareStatement("select * from blah");
* ...
* }
* ...
* }
*
* @param pstmt the statement to close.
* @throws SQLException if an exception occurs closing the statement
*/
public static void fastcloseStmt(PreparedStatement pstmt) throws SQLException
{
pstmt.close();
}
/**
* Closes a statement and a result set. This method should be called within the try section of
* your database logic when you reuse a statement. It may throw an exception,
* so don't place it in the finally section.
* Example:
*
*
* public void doSomething(Connection con) {
* PreparedStatement pstmt = null;
* try {
* pstmt = con.prepareStatement("select * from blah");
* rs = pstmt.executeQuery();
* ...
* ConnectionManager.fastcloseStmt(rs, pstmt);
* pstmt = con.prepareStatement("select * from blah");
* ...
* }
* ...
* }
*
* @param rs The result set to close
* @param pstmt the statement to close.
* @throws SQLException if an exception occurs closing the result set or statement
*/
public static void fastcloseStmt(ResultSet rs, PreparedStatement pstmt) throws SQLException
{
rs.close();
pstmt.close();
}
/**
* Closes a result set, statement and database connection (returning the connection to
* the connection pool). This method should be called within the finally section of
* your database logic, as in the following example:
*
*
* Connection con = null;
* PrepatedStatment pstmt = null;
* ResultSet rs = null;
* try {
* con = ConnectionManager.getConnection();
* pstmt = con.prepareStatement("select * from blah");
* rs = psmt.executeQuery();
* ....
* }
* catch (SQLException sqle) {
* Log.error(sqle.getMessage(), sqle);
* }
* finally {
* ConnectionManager.closeConnection(rs, pstmt, con);
* }
*
* @param rs the result set.
* @param stmt the statement.
* @param con the connection.
*/
public static void closeConnection(ResultSet rs, Statement stmt, Connection con) {
closeResultSet(rs);
closeStatement(stmt);
closeConnection(con);
}
/**
* Closes a statement and database connection (returning the connection to
* the connection pool). This method should be called within the finally section of
* your database logic, as in the following example:
*
* Connection con = null;
* PrepatedStatment pstmt = null;
* try {
* con = ConnectionManager.getConnection();
* pstmt = con.prepareStatement("select * from blah");
* ....
* }
* catch (SQLException sqle) {
* Log.error(sqle.getMessage(), sqle);
* }
* finally {
* DbConnectionManager.closeConnection(pstmt, con);
* }
*
* @param stmt the statement.
* @param con the connection.
*/
public static void closeConnection(Statement stmt, Connection con) {
closeStatement(stmt);
closeConnection(con);
}
/**
* Closes a database connection (returning the connection to the connection pool). Any
* statements associated with the connection should be closed before calling this method.
* This method should be called within the finally section of your database logic, as
* in the following example:
*
* Connection con = null;
* try {
* con = ConnectionManager.getConnection();
* ....
* }
* catch (SQLException sqle) {
* Log.error(sqle.getMessage(), sqle);
* }
* finally {
* DbConnectionManager.closeConnection(con);
* }
*
* @param con the connection.
*/
public static void closeConnection(Connection con) {
if (con != null) {
try {
con.close();
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
}
/**
* Creates a scroll insensitive PreparedStatement if the JDBC driver supports it, or a normal
* PreparedStatement otherwise.
*
* @param con the database connection.
* @param sql the SQL to create the PreparedStatement with.
* @return a PreparedStatement
* @throws java.sql.SQLException if an error occurs.
*/
public static PreparedStatement createScrollablePreparedStatement(Connection con, String sql)
throws SQLException {
if (isScrollResultsSupported()) {
return con.prepareStatement(sql, ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_READ_ONLY);
}
else {
return con.prepareStatement(sql);
}
}
/**
* Scrolls forward in a result set the specified number of rows. If the JDBC driver
* supports the feature, the cursor will be moved directly. Otherwise, we scroll
* through results one by one manually by calling {@code rs.next()}.
*
* @param rs the ResultSet object to scroll.
* @param rowNumber the row number to scroll forward to.
* @throws SQLException if an error occurs.
*/
public static void scrollResultSet(ResultSet rs, int rowNumber) throws SQLException {
// If the driver supports scrollable result sets, use that feature.
if (isScrollResultsSupported()) {
if (rowNumber > 0) {
// We will attempt to do a relative fetch. This may fail in SQL Server if
//