/*
* Copyright (C) 2004-2008 Jive Software, 2017-2026 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.Savepoint;
import java.sql.Statement;
import java.util.*;
import org.jivesoftware.util.ClassUtils;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.StringUtils;
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( Map errors ) {
boolean success = true;
try ( final Connection con = DbConnectionManager.getConnection() )
{
// See if the Jive db schema is installed.
try
{
Statement stmt = con.createStatement();
// Pick an arbitrary table to see if it's there.
stmt.executeQuery( "SELECT * FROM ofID" );
stmt.close();
}
catch ( SQLException sqle )
{
success = false;
Log.error( "The Openfire database schema does not appear to be installed.", sqle );
errors.put( "general", "The Openfire database schema does not "
+ "appear to be installed. Follow the installation guide to "
+ "fix this error." );
}
}
catch ( SQLException exception )
{
success = false;
Log.error( "Unable to connect to the database.", exception );
errors.put( "general", "A connection to the database could not be "
+ "made. View the error message by opening the "
+ "\"" + File.separator + "logs" + File.separator + "openfire.log\" log "
+ "file, then go back to fix the problem." );
}
return success;
}
/**
* Returns a database connection from the currently active connection
* provider. An exception will be thrown if no connection was found.
* (auto commit is set to true).
*
* @return a connection.
* @throws SQLException if a SQL exception occurs or no connection was found.
*/
public static Connection getConnection() throws SQLException {
ensureConnectionProvider();
int currentAttemptNumber = 0;
int maxRetries = JiveGlobals.getXMLProperty(SETTING_DATABASE_MAX_RETRIES, 10);
int retryWait = JiveGlobals.getXMLProperty(SETTING_DATABASE_RETRY_DELAY, 250); // milliseconds
SQLException lastException = null;
boolean loopIfNoConnection;
do {
currentAttemptNumber++;
try {
Connection con = connectionProvider.getConnection();
if (con != null) {
// Got one, lets hand it off.
// Usually profiling is not enabled. So we return a normal
// connection unless profiling is enabled. If yes, wrap the
// connection with a profiled connection.
if (!profilingEnabled) {
return con;
} else {
return new ProfiledConnection(con);
}
}
} catch (SQLException e) {
// TODO distinguish recoverable from non-recoverable exceptions.
lastException = e;
Log.info("Unable to get a connection from the database pool " +
"(attempt " + currentAttemptNumber + " out of " + (maxRetries + 1) + ").", e);
}
loopIfNoConnection = currentAttemptNumber < maxRetries + 1;
if (loopIfNoConnection) {
try {
Thread.sleep(retryWait);
} catch (InterruptedException ex) {
String msg = "Interrupted waiting for DB connection";
Log.info(msg,ex);
Thread.currentThread().interrupt();
throw new SQLException(msg,ex);
}
}
} while (loopIfNoConnection);
throw new SQLException("ConnectionManager.getConnection() " +
"failed to obtain a connection after " + currentAttemptNumber + " attempts. " +
"The exception from the last attempt is as follows: " + lastException);
}
/**
* Returns a Connection from the currently active connection provider that
* is ready to participate in transactions (auto commit is set to false).
*
* @return a connection with transactions enabled.
* @throws SQLException if a SQL exception occurs.
*/
public static Connection getTransactionConnection() throws SQLException {
Connection con = getConnection();
if (isTransactionsSupported()) {
con.setAutoCommit(false);
}
return con;
}
/**
* Closes a PreparedStatement and Connection. However, it first rolls back the transaction or
* commits it depending on the value of abortTransaction.
*
* @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
// is set to absolute. It would need to be
// set to looping to work correctly.
// If so, manually scroll to the correct row.
try {
rs.setFetchDirection(ResultSet.FETCH_FORWARD);
rs.relative(rowNumber);
}
catch (SQLException e) {
// TODO change "Error ..." to "Disabling ..."
Log.error("Error in JDBC method rs.relative(rowNumber).", e);
//Log.error("Disabling JDBC method rs.relative(rowNumber).", e);
//scrollResultsSupported = false;
for (int i = 0; i < rowNumber; i++) {
rs.next();
}
}
}
}
// Otherwise, manually scroll to the correct row.
else {
for (int i = 0; i < rowNumber; i++) {
rs.next();
}
}
}
/**
* Limits the number of the results in a result set (to startIndex + numResults).
* Sets the fetch size depending on the features of the JDBC driver and make
* sure that the size is not bigger than 500.
* @param pstmt the PreparedStatement
* @param startIndex the first row with interesting data
* @param numResults the number of interesting results
*/
public static void limitRowsAndFetchSize(PreparedStatement pstmt, int startIndex, int numResults) {
final int MAX_FETCHRESULTS = 500;
final int maxRows = startIndex + numResults;
setMaxRows(pstmt, maxRows);
if (pstmt_fetchSizeSupported)
{
if (scrollResultsSupported) {
setFetchSize(pstmt, Math.min(MAX_FETCHRESULTS, numResults));
}
else {
setFetchSize(pstmt, Math.min(MAX_FETCHRESULTS, maxRows));
}
}
}
/**
* Sets the number of rows that the JDBC driver should buffer at a time.
* The operation is automatically bypassed if Openfire knows that the
* the JDBC driver or database doesn't support it.
*
* @param pstmt the PreparedStatement to set the fetch size for.
* @param fetchSize the fetchSize.
*/
public static void setFetchSize(PreparedStatement pstmt, int fetchSize) {
if (pstmt_fetchSizeSupported) {
try {
pstmt.setFetchSize(fetchSize);
}
catch (Throwable t) {
// Ignore. Exception may happen if the driver doesn't support
// this operation and we didn't set meta-data correctly.
// However, it is a good idea to update the meta-data so that
// we don't have to incur the cost of catching an exception
// each time.
Log.debug("Disabling JDBC method pstmt.setFetchSize(fetchSize).", t);
pstmt_fetchSizeSupported = false;
}
}
}
/**
* Returns the current connection provider. The only case in which this
* method should be called is if more information about the current
* connection provider is needed. Database connections should always be
* obtained by calling the getConnection method of this class.
*
* @return the connection provider.
*/
public static ConnectionProvider getConnectionProvider() {
return connectionProvider;
}
/**
* Sets the connection provider. The old provider (if it exists) is shut
* down before the new one is started. A connection provider should
* not be started before being passed to the connection manager
* because the manager will call the start() method automatically.
*
* @param provider the ConnectionProvider that the manager should obtain
* connections from.
*/
public static void setConnectionProvider(ConnectionProvider provider) {
synchronized (providerLock) {
if (connectionProvider != null) {
connectionProvider.destroy();
connectionProvider = null;
}
connectionProvider = provider;
connectionProvider.start();
// Now, get a connection to determine meta data.
Connection con = null;
try {
con = connectionProvider.getConnection();
setMetaData(con);
// Check to see if the database schema needs to be upgraded.
schemaManager.checkOpenfireSchema(con);
// Probe string-based large text I/O only after the Openfire schema has been verified.
if (databaseType == DatabaseType.oracle) {
final DatabaseMetaData metaData = con.getMetaData();
final String driverName = metaData.getDriverName().toLowerCase(Locale.ROOT);
final int driverMajor = metaData.getDriverMajorVersion();
if (driverMajor >= 12 && !driverName.contains("auguro")) {
streamTextRequired = probeStreamingRequired(con);
}
}
}
catch (MissingResourceException mre) {
Log.error(mre.getMessage());
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
finally {
closeConnection(con);
}
}
// Remember what connection provider we want to use for restarts.
JiveGlobals.setXMLProperty("connectionProvider.className", provider.getClass().getName());
}
/**
* Destroys the currennt connection provider. Future calls to
* {@link #getConnectionProvider()} will return {@code null} until a new
* ConnectionProvider is set, or one is automatically loaded by a call to
* {@link #getConnection()}.
*/
public static void destroyConnectionProvider() {
synchronized (providerLock) {
if (connectionProvider != null) {
connectionProvider.destroy();
connectionProvider = null;
}
}
}
/**
* Retrives a large text column from a result set, automatically performing
* streaming if the JDBC driver requires it. This is necessary because
* different JDBC drivers have different capabilities and methods for
* retrieving large text values.
*
* @param rs the ResultSet to retrieve the text field from.
* @param columnIndex the column in the ResultSet of the text field.
* @return the String value of the text field.
* @throws SQLException if an SQL exception occurs.
*/
public static String getLargeTextField(ResultSet rs, int columnIndex) throws SQLException {
if (isStreamTextRequired()) {
String value;
try (Reader bodyReader = rs.getCharacterStream(columnIndex)) {
if (bodyReader == null) {
return null;
}
char[] buf = new char[256];
int len;
StringWriter out = new StringWriter(256);
while ((len = bodyReader.read(buf)) >= 0) {
out.write(buf, 0, len);
}
value = out.toString();
out.close();
}
catch (Exception e) {
Log.error(e.getMessage(), e);
throw new SQLException("Failed to load text field");
}
return value;
}
else {
return rs.getString(columnIndex);
}
}
/**
* Sets a large text column in a result set, automatically performing
* streaming if the JDBC driver requires it. This is necessary because
* different JDBC drivers have different capabilities and methods for
* setting large text values.
*
* @param pstmt the PreparedStatement to set the text field in.
* @param parameterIndex the index corresponding to the text field.
* @param value the String to set.
* @throws SQLException if an SQL exception occurs.
*/
public static void setLargeTextField(PreparedStatement pstmt, int parameterIndex,
String value) throws SQLException {
if (isStreamTextRequired()) {
Reader bodyReader;
try {
bodyReader = new StringReader(value);
pstmt.setCharacterStream(parameterIndex, bodyReader, value.length());
}
catch (Exception e) {
Log.error(e.getMessage(), e);
throw new SQLException("Failed to set text field.");
}
// Leave bodyReader open so that the db can read from it. It *should*
// be garbage collected after it's done without needing to call close.
}
else {
pstmt.setString(parameterIndex, value);
}
}
/**
* Sets the max number of rows that should be returned from executing a
* statement. The operation is automatically bypassed if Jive knows that the
* the JDBC driver or database doesn't support it.
*
* @param stmt the Statement to set the max number of rows for.
* @param maxRows the max number of rows to return.
*/
public static void setMaxRows(Statement stmt, int maxRows) {
if (isMaxRowsSupported()) {
try {
stmt.setMaxRows(maxRows);
}
catch (Throwable t) {
// Ignore. Exception may happen if the driver doesn't support
// this operation and we didn't set meta-data correctly.
// However, it is a good idea to update the meta-data so that
// we don't have to incur the cost of catching an exception
// each time.
Log.debug("Disabling JDBC method stmt.setMaxRows(maxRows).", t);
maxRowsSupported = false;
}
}
}
/**
* Sets the number of rows that the JDBC driver should buffer at a time.
* The operation is automatically bypassed if Jive knows that the
* the JDBC driver or database doesn't support it.
*
* @param rs the ResultSet to set the fetch size for.
* @param fetchSize the fetchSize.
*/
public static void setFetchSize(ResultSet rs, int fetchSize) {
if (isFetchSizeSupported()) {
try {
rs.setFetchSize(fetchSize);
}
catch (Throwable t) {
// Ignore. Exception may happen if the driver doesn't support
// this operation and we didn't set meta-data correctly.
// However, it is a good idea to update the meta-data so that
// we don't have to incur the cost of catching an exception
// each time.
Log.debug("Disabling JDBC method rs.setFetchSize(fetchSize).", t);
fetchSizeSupported = false;
}
}
}
/**
* Returns a SchemaManager instance, which can be used to manage the database
* schema information for Openfire and plugins.
*
* @return a SchemaManager instance.
*/
public static SchemaManager getSchemaManager() {
return schemaManager;
}
/**
* Uses a connection from the database to set meta data information about
* what different JDBC drivers and databases support.
*
* @param con the connection.
* @throws SQLException if an SQL exception occurs.
*/
private static void setMetaData(Connection con) throws SQLException {
DatabaseMetaData metaData = con.getMetaData();
// Log what we're connecting to so operators can diagnose capability issues.
Log.info("Detected database: {} {} | Driver: {} {}.{}",
metaData.getDatabaseProductName(),
metaData.getDatabaseProductVersion(),
metaData.getDriverName(),
metaData.getDriverMajorVersion(),
metaData.getDriverMinorVersion());
// Standard metadata queries (reliable across all modern drivers)
transactionsSupported = metaData.supportsTransactions();
subqueriesSupported = metaData.supportsCorrelatedSubqueries();
batchUpdatesSupported = metaData.supportsBatchUpdates();
identifierQuoteString = metaData.getIdentifierQuoteString();
try {
scrollResultsSupported = metaData.supportsResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE);
} catch (Exception e) {
// Workaround for DB2 JDBC, which throws on this call.
scrollResultsSupported = false;
}
// Conservative defaults for flags not covered by standard metadata. These may be tightened per-database below.
streamTextRequired = false;
maxRowsSupported = true;
fetchSizeSupported = true;
String dbName = metaData.getDatabaseProductName().toLowerCase(Locale.ROOT);
String driverName = metaData.getDriverName().toLowerCase(Locale.ROOT);
int driverMajor = metaData.getDriverMajorVersion();
// Oracle
if (dbName.contains("oracle")) {
databaseType = DatabaseType.oracle;
// Oracle JDBC 12.1+ (ojdbc8/ojdbc11) can read moderate-sized CLOBs via getString() without streaming.
// Below 12, streaming is required to avoid ORA-17023 / ORA-17011 on CLOB columns. Very large CLOBs
// (> ~32 KB) may still need streaming on all versions. For newer drivers, a best-effort probe against
// a CLOB-backed Openfire table is executed later, after schema verification.
if (driverMajor < 12) {
streamTextRequired = true;
}
// i-net AUGURO is an old third-party Oracle driver. Still harmless to keep.
if (driverName.contains("auguro")) {
streamTextRequired = false;
fetchSizeSupported = true;
maxRowsSupported = false;
}
}
// PostgreSQL
else if (dbName.contains("postgres")) {
databaseType = DatabaseType.postgresql;
// PostgreSQL JDBC 42.x reports TYPE_SCROLL_INSENSITIVE as supported, which is accurate - but server-side
// cursors (required for scrolling) only work when auto-commit is off. Since many connections in this
// codebase use auto-commit, we leave scrollResultsSupported as-is from metadata (true) and let the runtime
// try/catch in scrollResultSet() handle any failure gracefully.
// setFetchSize() is silently ignored by the PostgreSQL JDBC driver when auto-commit is on, so it will never
// throw to trigger the try/catch auto-detect. On transactional (auto-commit = false) connections it
// works correctly. Leaving fetchSizeSupported=true means we call it everywhere; it is a no-op for
// auto-commit connections and effective for transactional ones. Preferable to blanket false.
}
// Interbase (Embarcadero)
// Note: Firebird (the open-source fork) reports product name "Firebird", not "Interbase", so Jaybird 5.x
// installations are NOT matched here. They inherit the defaults above, which are correct for Jaybird.
else if (dbName.contains("interbase")) {
databaseType = DatabaseType.interbase;
fetchSizeSupported = false;
maxRowsSupported = false;
}
// Microsoft SQL Server
// The Microsoft JDBC driver (mssql-jdbc) supports both fetchSize and maxRows correctly and will be detected and
// handled by the global try/catch on first use.
else if (dbName.contains("sql server")) {
databaseType = DatabaseType.sqlserver;
}
// MySQL / MariaDB
else if (dbName.contains("mysql") || dbName.contains("maria")) {
databaseType = DatabaseType.mysql;
}
// HSQLDB
else if (dbName.contains("hsql")) {
databaseType = DatabaseType.hsqldb;
}
// DB2
else if (dbName.contains("db2")) {
databaseType = DatabaseType.db2;
}
Log.debug("Database capabilities: transactions={}, scrollResults={}, batchUpdates={}, subqueries={}, streamText={}, maxRows={}, fetchSize={}, identifierQuoteString={}",
transactionsSupported, scrollResultsSupported, batchUpdatesSupported, subqueriesSupported, streamTextRequired, maxRowsSupported, fetchSizeSupported, identifierQuoteString);
}
/**
* Probes whether the database/driver requires text to be written and read via character streams rather than
* plain getString()/setString().
*
* This is needed when the JDBC driver cannot natively coerce between its CLOB/NCLOB/TEXT type and a Java String for
* large values. The probe writes a large string (8192 characters) to the CLOB-backed {@code ofPubsubItem.payload}
* column and reads it back; if the driver throws, returns null, or returns a different value, streaming is needed.
*
* This is a best-effort probe: if the probe itself fails for an unexpected reason, we conservatively return
* {@code true} (streaming required).
*
* @param con an open connection to probe with.
* @return true if streaming is required, false if getString()/setString() work.
*/
private static boolean probeStreamingRequired(Connection con)
{
// Use per-run identifiers to avoid mutating or deleting any user-provided data.
final String probeKey = "streaming-probe-" + UUID.randomUUID();
final String probeJid = "streaming-probe@example.org";
final String probeCreationDate = StringUtils.dateToMillis(new Date());
final String probeValue = StringUtils.randomString(8192);
final boolean autoCommit;
Savepoint probeSavepoint = null;
try {
autoCommit = con.getAutoCommit();
} catch (SQLException ex) {
Log.debug("Unable to determine auto-commit state for database streaming probe; defaulting to stream-based large text I/O.", ex);
return true;
}
// When auto-commit is disabled, isolate all probe writes in a savepoint and roll them back afterwards.
// Use an unnamed savepoint for widest JDBC compatibility.
if (!autoCommit) {
try {
probeSavepoint = con.setSavepoint();
} catch (SQLException ex) {
Log.debug("Unable to create savepoint for database streaming probe on non-autocommit connection; defaulting to stream-based large text I/O.", ex);
return true;
}
}
// Insert only. Updating shouldn't be needed, as a per-run unique key is used.
boolean probeRowInserted = false;
try (PreparedStatement insert = con.prepareStatement("INSERT INTO ofPubsubItem (serviceID, nodeID, id, jid, creationDate, payload) VALUES (?, ?, ?, ?, ?, ?)")) {
insert.setString(1, probeKey);
insert.setString(2, probeKey);
insert.setString(3, probeKey);
insert.setString(4, probeJid);
insert.setString(5, probeCreationDate);
insert.setString(6, probeValue);
probeRowInserted = insert.executeUpdate() > 0;
} catch (SQLException ex) {
Log.debug("Unable to insert a test row used for the database streaming probe.", ex);
}
if (!probeRowInserted) {
if (probeSavepoint != null) {
try {
con.rollback(probeSavepoint);
} catch (SQLException ex) {
Log.debug("Unable to roll back savepoint for failed database streaming probe.", ex);
}
try {
con.releaseSavepoint(probeSavepoint);
} catch (SQLException ex) {
Log.debug("Unable to release savepoint for failed database streaming probe.", ex);
}
}
Log.debug("Database streaming probe row could not be prepared; defaulting to stream-based large text I/O.");
return true;
}
try (PreparedStatement read = con.prepareStatement("SELECT payload FROM ofPubsubItem WHERE serviceID = ? AND nodeID = ? AND id = ?")) {
read.setString(1, probeKey);
read.setString(2, probeKey);
read.setString(3, probeKey);
try (ResultSet rs = read.executeQuery()) {
if (!rs.next()) {
Log.debug("Database streaming probe row was not found after insertion; defaulting to stream-based large text I/O.");
return true;
}
final String result = rs.getString(1);
// Streaming is required if: exception occurs (caught below), result is null, or result differs from original.
boolean streamingRequired = result == null || !probeValue.equals(result);
if (streamingRequired) {
Log.debug("Database streaming probe detected that getString()/setString() cannot reliably handle large text values; enabling stream-based I/O.");
}
return streamingRequired;
}
} catch (SQLException e) {
Log.debug("Streaming probe via setString()/getString() failed; enabling stream-based large text I/O.", e);
return true;
} catch (Exception e) {
Log.info("Unexpected error during database streaming probe; defaulting to streaming.", e);
return true;
} finally {
if (probeSavepoint != null) {
// On non-autocommit connections, rollback to savepoint to release locks and remove probe side effects.
try {
con.rollback(probeSavepoint);
} catch (SQLException ex) {
Log.debug("Unable to rollback savepoint used by the database streaming probe.", ex);
}
try {
con.releaseSavepoint(probeSavepoint);
} catch (SQLException ex) {
Log.debug("Unable to release savepoint used by the database streaming probe.", ex);
}
} else {
// Cleanup our test row for auto-commit connections.
try (PreparedStatement delete = con.prepareStatement("DELETE FROM ofPubsubItem WHERE serviceID = ? AND nodeID = ? AND id = ?")) {
delete.setString(1, probeKey);
delete.setString(2, probeKey);
delete.setString(3, probeKey);
delete.execute();
} catch (SQLException ex) {
Log.debug("Unable to clean up a test row used by the database streaming probe.", ex);
}
}
}
}
/**
* Returns the database type. The possible types are constants of the
* DatabaseType class. Any database that doesn't have its own constant
* falls into the "Other" category.
*
* @return the database type.
*/
public static DatabaseType getDatabaseType() {
return databaseType;
}
/**
* Returns true if connection profiling is turned on. You can collect
* profiling statistics by using the static methods of the ProfiledConnection
* class.
*
* @return true if connection profiling is enabled.
*/
public static boolean isProfilingEnabled() {
return profilingEnabled;
}
/**
* Turns connection profiling on or off. You can collect profiling
* statistics by using the static methods of the ProfiledConnection
* class.
*
* @param enable true to enable profiling; false to disable.
*/
public static void setProfilingEnabled(boolean enable) {
// If enabling profiling, call the start method on ProfiledConnection
if (!profilingEnabled && enable) {
ProfiledConnection.start();
}
// Otherwise, if turning off, call stop method.
else if (profilingEnabled && !enable) {
ProfiledConnection.stop();
}
profilingEnabled = enable;
}
public static boolean isTransactionsSupported() {
return transactionsSupported;
}
public static boolean isStreamTextRequired() {
return streamTextRequired;
}
public static boolean isMaxRowsSupported() {
return maxRowsSupported;
}
public static boolean isFetchSizeSupported() {
return fetchSizeSupported;
}
public static boolean isPstmtFetchSizeSupported() {
return pstmt_fetchSizeSupported;
}
public static boolean isSubqueriesSupported() {
return subqueriesSupported;
}
public static boolean isScrollResultsSupported() {
return scrollResultsSupported;
}
public static boolean isBatchUpdatesSupported() {
return batchUpdatesSupported;
}
public static boolean isEmbeddedDB() {
return connectionProvider instanceof EmbeddedConnectionProvider;
}
public static String getIdentifierQuoteString() {
return identifierQuoteString;
}
public static String getTestSQL(String driver) {
if (driver == null) {
return "select 1";
}
else if (driver.contains("db2")) {
return "select 1 from sysibm.sysdummy1";
}
else if (driver.contains("oracle")) {
return "select 1 from dual";
}
else {
return "select 1";
}
}
/**
* A class that identifies the type of the database that Jive is connected
* to. In most cases, we don't want to make any database specific calls
* and have no need to know the type of database we're using. However,
* there are certain cases where it's critical to know the database for
* performance reasons.
*/
public enum DatabaseType {
oracle,
postgresql,
mysql("rank"),
hsqldb,
db2,
sqlserver,
interbase,
unknown;
private final HashSet identifiers;
DatabaseType(final String ... identifiers) {
this.identifiers = new HashSet<>(Arrays.asList(identifiers));
}
public String escapeIdentifier(final String keyword) {
if (identifiers.contains(keyword)) {
return String.format("%1$s%2$s%1$s", DbConnectionManager.getIdentifierQuoteString(), keyword);
} else {
return keyword;
}
}
}
}