/*
* Copyright (C) 2005-2008 Jive Software, 2016-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 org.apache.commons.dbcp2.*;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.jivesoftware.util.JiveGlobals;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.SQLException;
import java.text.DecimalFormat;
import java.time.Duration;
/**
* Default Jive connection provider, which uses an internal connection pool.
*
* @author Jive Software
*/
public class DefaultConnectionProvider implements ConnectionProvider {
private static final Logger Log = LoggerFactory.getLogger(DefaultConnectionProvider.class);
private String driver;
private String serverURL;
private String username;
private String password;
private int minConnections = 3;
private int maxConnections = 10;
private String testSQL = "";
private Boolean testBeforeUse = true;
private Boolean testAfterUse = true;
private Duration testTimeout = Duration.ofMillis(500);
private Duration timeBetweenEvictionRuns = Duration.ofSeconds(30);
private Duration minIdleTime = Duration.ofMinutes(15);
private Duration maxWaitTime = Duration.ofMillis(500);
private long refusedCount = 0;
private PoolingDataSource dataSource;
private GenericObjectPool connectionPool;
/**
* Maximum time a connection can be open before it's reopened.
*/
private Duration connectionTimeout = Duration.ofHours(12);
/**
* MySQL doesn't currently support Unicode. However, a workaround is
* implemented in the mm.mysql JDBC driver. Setting the Jive property
* database.mysql.useUnicode to true will turn this feature on.
*/
private boolean mysqlUseUnicode;
/**
* Creates a new DefaultConnectionProvider.
*/
public DefaultConnectionProvider() {
loadProperties();
}
@Override
public boolean isPooled() {
return true;
}
@Override
public Connection getConnection() throws SQLException {
if (dataSource == null) {
throw new SQLException("Check JDBC properties; data source was not be initialised");
}
// DBCP doesn't expose the number of refused connections, so count them ourselves
try {
return dataSource.getConnection();
} catch (final SQLException e) {
refusedCount++;
throw e;
}
}
@Override
public void start() {
try {
Class.forName(driver);
} catch (final ClassNotFoundException e) {
throw new RuntimeException("Unable to find JDBC driver " + driver, e);
}
final ConnectionFactory connectionFactory = new DriverManagerConnectionFactory(serverURL, username, password);
final PoolableConnectionFactory poolableConnectionFactory = new PoolableConnectionFactory(connectionFactory, null);
poolableConnectionFactory.setValidationQuery(testSQL);
poolableConnectionFactory.setValidationQueryTimeout(testTimeout);
poolableConnectionFactory.setMaxConn(connectionTimeout);
final GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig<>();
poolConfig.setTestOnBorrow(testBeforeUse);
poolConfig.setTestOnReturn(testAfterUse);
poolConfig.setMinIdle(minConnections);
if( minConnections > GenericObjectPoolConfig.DEFAULT_MAX_IDLE )
{
poolConfig.setMaxIdle(minConnections);
}
poolConfig.setMaxTotal(maxConnections);
poolConfig.setTimeBetweenEvictionRuns(timeBetweenEvictionRuns);
poolConfig.setSoftMinEvictableIdleDuration(minIdleTime);
poolConfig.setMaxWait(maxWaitTime);
connectionPool = new GenericObjectPool<>(poolableConnectionFactory, poolConfig);
poolableConnectionFactory.setPool(connectionPool);
dataSource = new PoolingDataSource<>(connectionPool);
}
@Override
public void restart() {
}
@Override
public void destroy() {
try {
if (dataSource != null) {
dataSource.close();
}
} catch (final Exception e) {
Log.error("Unable to close the data source", e);
}
}
/**
* Returns the JDBC driver classname used to make database connections.
* For example: com.mysql.jdbc.Driver
*
* @return the JDBC driver classname.
*/
public String getDriver() {
return driver;
}
/**
* Sets the JDBC driver classname used to make database connections.
* For example: com.mysql.jdbc.Driver
*
* @param driver the fully qualified JDBC driver name.
*/
public void setDriver(String driver) {
this.driver = driver;
saveProperties();
}
/**
* Returns the JDBC connection URL used to make database connections.
*
* @return the JDBC connection URL.
*/
public String getServerURL() {
return serverURL;
}
/**
* Sets the JDBC connection URL used to make database connections.
*
* @param serverURL the JDBC connection URL.
*/
public void setServerURL(String serverURL) {
this.serverURL = serverURL;
saveProperties();
}
/**
* Returns the username used to connect to the database. In some cases,
* a username is not needed so this method will return null.
*
* @return the username used to connect to the database.
*/
public String getUsername() {
return username;
}
/**
* Sets the username used to connect to the database. In some cases, a
* username is not needed so null should be passed in.
*
* @param username the username used to connect to the database.
*/
public void setUsername(String username) {
this.username = username;
saveProperties();
}
/**
* Returns the password used to connect to the database. In some cases,
* a password is not needed so this method will return null.
*
* @return the password used to connect to the database.
*/
public String getPassword() {
return password;
}
/**
* Sets the password used to connect to the database. In some cases, a
* password is not needed so null should be passed in.
*
* @param password the password used to connect to the database.
*/
public void setPassword(String password) {
this.password = password;
saveProperties();
}
/**
* Returns the minimum number of connections that the pool will use. This
* should probably be at least three.
*
* @return the minimum number of connections in the pool.
*/
public int getMinConnections() {
return minConnections;
}
/**
* Sets the minimum number of connections that the pool will use. This
* should probably be at least three.
*
* @param minConnections the minimum number of connections in the pool.
*/
public void setMinConnections(int minConnections) {
this.minConnections = minConnections;
saveProperties();
}
/**
* Returns the maximum number of connections that the pool will use. The
* actual number of connections in the pool will vary between this value
* and the minimum based on the current load.
*
* @return the max possible number of connections in the pool.
*/
public int getMaxConnections() {
return maxConnections;
}
/**
* Sets the maximum number of connections that the pool will use. The
* actual number of connections in the pool will vary between this value
* and the minimum based on the current load.
*
* @param maxConnections the max possible number of connections in the pool.
*/
public void setMaxConnections(int maxConnections) {
this.maxConnections = maxConnections;
saveProperties();
}
/**
* Returns the amount of time between connection recycles in days. For
* example, a value of .5 would correspond to recycling the connections
* in the pool once every half day.
*
* @return the amount of time in days between connection recycles.
*/
public Duration getConnectionTimeout() {
return connectionTimeout;
}
/**
* Sets the amount of time between connection recycles.
*
* @param connectionTimeout the amount of time between connection recycles.
*/
public void setConnectionTimeout(Duration connectionTimeout) {
this.connectionTimeout = connectionTimeout;
saveProperties();
}
/**
* Returns the SQL statement used to test if a connection is valid.
*
* @return the SQL statement that will be run to test a connection.
*/
public String getTestSQL() {
return testSQL;
}
public Duration getTestTimeout() {
return testTimeout;
}
/**
* @deprecated Replaced by {@link #getDurationBetweenEvictionRuns()}
*/
@Deprecated
public Duration getTimeBetweenEvictionRunsMillis() {
return getDurationBetweenEvictionRuns();
}
public Duration getDurationBetweenEvictionRuns() {
return connectionPool.getDurationBetweenEvictionRuns();
}
public Duration getMinIdleTime() {
return connectionPool.getSoftMinEvictableIdleDuration();
}
public int getActiveConnections() {
return connectionPool.getNumActive();
}
public int getIdleConnections() {
return connectionPool.getNumIdle();
}
public long getConnectionsServed() {
return connectionPool.getBorrowedCount();
}
public long getRefusedCount() {
return refusedCount;
}
public Duration getMaxWaitTime() {
return connectionPool.getMaxWaitDuration();
}
public Duration getMeanBorrowWaitTime() {
return connectionPool.getMeanBorrowWaitDuration();
}
public Duration getMaxBorrowWaitTime() {
return connectionPool.getMaxBorrowWaitDuration();
}
/**
* Sets the SQL statement used to test if a connection is valid. House keeping
* and before/after connection tests make use of this. This
* should be something that causes the minimal amount of work by the database
* server and is as quick as possible.
*
* @param testSQL the SQL statement that will be run to test a connection.
*/
public void setTestSQL(String testSQL) {
this.testSQL = testSQL;
}
/**
* Returns whether returned connections will be tested before being handed over
* to be used.
*
* @return True if connections are tested before use.
*/
public Boolean getTestBeforeUse() {
return testBeforeUse;
}
/**
* Sets whether connections will be tested before being handed over to be used.
*
* @param testBeforeUse True or false if connections are to be tested before use.
*/
public void setTestBeforeUse(Boolean testBeforeUse) {
this.testBeforeUse = testBeforeUse;
}
/**
* Returns whether returned connections will be tested after being returned to
* the pool.
*
* @return True if connections are tested after use.
*/
public Boolean getTestAfterUse() {
return testAfterUse;
}
/**
* Sets whether connections will be tested after being returned to the pool.
*
* @param testAfterUse True or false if connections are to be tested after use.
*/
public void setTestAfterUse(Boolean testAfterUse) {
this.testAfterUse = testAfterUse;
}
public boolean isMysqlUseUnicode() {
return mysqlUseUnicode;
}
/**
* Load properties that already exist from Jive properties.
*/
private void loadProperties() {
driver = JiveGlobals.getXMLProperty("database.defaultProvider.driver");
serverURL = JiveGlobals.getXMLProperty("database.defaultProvider.serverURL");
username = JiveGlobals.getXMLProperty("database.defaultProvider.username");
password = JiveGlobals.getXMLProperty("database.defaultProvider.password");
String minCons = JiveGlobals.getXMLProperty("database.defaultProvider.minConnections");
String maxCons = JiveGlobals.getXMLProperty("database.defaultProvider.maxConnections");
String conTimeout = JiveGlobals.getXMLProperty("database.defaultProvider.connectionTimeout");
testSQL = JiveGlobals.getXMLProperty("database.defaultProvider.testSQL", DbConnectionManager.getTestSQL(driver));
testBeforeUse = JiveGlobals.getXMLProperty("database.defaultProvider.testBeforeUse", false);
testAfterUse = JiveGlobals.getXMLProperty("database.defaultProvider.testAfterUse", false);
testTimeout = Duration.ofMillis(JiveGlobals.getXMLProperty("database.defaultProvider.testTimeout", (int) Duration.ofMillis(500).toMillis()));
timeBetweenEvictionRuns = Duration.ofMillis(JiveGlobals.getXMLProperty("database.defaultProvider.timeBetweenEvictionRuns", (int) (Duration.ofSeconds(30).toMillis())));
minIdleTime = Duration.ofMillis(JiveGlobals.getXMLProperty("database.defaultProvider.minIdleTime", (int) (Duration.ofMinutes(15)).toMillis()));
maxWaitTime = Duration.ofMillis(JiveGlobals.getXMLProperty("database.defaultProvider.maxWaitTime", (int) Duration.ofMillis(500).toMillis()));
// See if we should use Unicode under MySQL
mysqlUseUnicode = Boolean.parseBoolean(JiveGlobals.getXMLProperty("database.mysql.useUnicode"));
try {
if (minCons != null) {
minConnections = Integer.parseInt(minCons);
}
if (maxCons != null) {
maxConnections = Integer.parseInt(maxCons);
}
if (conTimeout != null) {
connectionTimeout = Duration.ofMinutes( Math.round(Double.parseDouble(conTimeout) * 24 * 60));
}
}
catch (Exception e) {
Log.error("Error: could not parse default pool properties. " +
"Make sure the values exist and are correct.", e);
}
}
/**
* Save properties as Jive properties.
*/
private void saveProperties() {
JiveGlobals.setXMLProperty("database.defaultProvider.driver", driver);
JiveGlobals.setXMLProperty("database.defaultProvider.serverURL", serverURL);
JiveGlobals.setXMLProperty("database.defaultProvider.username", username);
JiveGlobals.setXMLProperty("database.defaultProvider.password", password);
JiveGlobals.setXMLProperty("database.defaultProvider.testSQL", testSQL);
JiveGlobals.setXMLProperty("database.defaultProvider.testBeforeUse", testBeforeUse.toString());
JiveGlobals.setXMLProperty("database.defaultProvider.testAfterUse", testAfterUse.toString());
JiveGlobals.setXMLProperty("database.defaultProvider.testTimeout", String.valueOf(testTimeout));
JiveGlobals.setXMLProperty("database.defaultProvider.timeBetweenEvictionRuns", String.valueOf(timeBetweenEvictionRuns));
JiveGlobals.setXMLProperty("database.defaultProvider.minIdleTime", String.valueOf(minIdleTime));
JiveGlobals.setXMLProperty("database.defaultProvider.maxWaitTime", String.valueOf(maxWaitTime));
JiveGlobals.setXMLProperty("database.defaultProvider.minConnections", Integer.toString(minConnections));
JiveGlobals.setXMLProperty("database.defaultProvider.maxConnections", Integer.toString(maxConnections));
JiveGlobals.setXMLProperty("database.defaultProvider.connectionTimeout", new DecimalFormat("#.0").format(connectionTimeout.toMinutes() / 60.0 / 24));
}
}