/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package com.xpn.xwiki.store;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Iterator;
import java.util.Properties;
import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.commons.dbcp2.BasicDataSourceFactory;
import org.hibernate.HibernateException;
import org.hibernate.cfg.Environment;
import org.hibernate.connection.ConnectionProvider;
import org.hibernate.connection.ConnectionProviderFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>
* A connection provider that uses an Apache commons DBCP connection pool.
* </p>
* <p>
* To use this connection provider set:<br>
* <code>hibernate.connection.provider_class org.hibernate.connection.DBCPConnectionProvider</code>
* </p>
*
* <pre>
* Supported Hibernate properties:
* hibernate.connection.driver_class
* hibernate.connection.url
* hibernate.connection.username
* hibernate.connection.password
* hibernate.connection.isolation
* hibernate.connection.autocommit
* hibernate.connection.pool_size
* hibernate.connection (JDBC driver properties)
* </pre>
*
* <br>
* All DBCP properties are also supported by using the hibernate.dbcp prefix. A complete list can be found on the DBCP
* configuration page: <a
* href="http://jakarta.apache.org/commons/dbcp/configuration.html">http://jakarta.apache.org/commons
* /dbcp/configuration.html</a>. <br>
*
* <pre>
* Example:
* hibernate.connection.provider_class org.hibernate.connection.DBCPConnectionProvider
* hibernate.connection.driver_class org.hsqldb.jdbcDriver
* hibernate.connection.username sa
* hibernate.connection.password
* hibernate.connection.url jdbc:hsqldb:test
* hibernate.connection.pool_size 20
* hibernate.dbcp.initialSize 10
* hibernate.dbcp.maxWait 3000
* hibernate.dbcp.validationQuery select 1 from dual
* </pre>
* <p>
* More information about configuring/using DBCP can be found on the <a
* href="http://jakarta.apache.org/commons/dbcp/">DBCP website</a>. There you will also find the DBCP wiki, mailing
* lists, issue tracking and other support facilities
* </p>
*
* @see org.hibernate.connection.ConnectionProvider
* @author Dirk Verbeeck
*/
public class DBCPConnectionProvider implements ConnectionProvider
{
/**
* Logger to use to log shutdown information (opposite of initialization).
*/
private static final Logger SHUTDOWN_LOGGER = LoggerFactory.getLogger("org.xwiki.shutdown");
private static final Logger LOGGER = LoggerFactory.getLogger(DBCPConnectionProvider.class);
private static final String PREFIX = "hibernate.dbcp.";
private BasicDataSource ds;
// Old Environment property for backward-compatibility (property removed in Hibernate3)
private static final String COMPATIBILITY_PS_MAXACTIVE = "ps.maxActive";
// Properties removed from DBCP 2.0
private static final String COMPATIBILITY_MAXACTIVE = "maxActive";
private static final String COMPATIBILITY_MAXWAIT = "maxWait";
// Property doesn't exists in Hibernate2
private static final String AUTOCOMMIT = "hibernate.connection.autocommit";
@Override
public void configure(Properties props) throws HibernateException
{
try {
LOGGER.debug("Configure DBCPConnectionProvider");
// DBCP properties used to create the BasicDataSource
Properties dbcpProperties = new Properties();
// DriverClass & url
String jdbcDriverClass = props.getProperty(Environment.DRIVER);
dbcpProperties.put("driverClassName", jdbcDriverClass);
String jdbcUrl = System.getProperty(Environment.URL);
if (jdbcUrl == null) {
jdbcUrl = props.getProperty(Environment.URL);
}
dbcpProperties.put("url", jdbcUrl);
// Username / password. Only put username and password if they're not null. This allows
// external authentication support (OS authenticated). It'll thus work if the hibernate
// config does not specify a username and/or password.
String username = props.getProperty(Environment.USER);
if (username != null) {
dbcpProperties.put("username", username);
}
String password = props.getProperty(Environment.PASS);
if (password != null) {
dbcpProperties.put("password", password);
}
// Isolation level
String isolationLevel = props.getProperty(Environment.ISOLATION);
if ((isolationLevel != null) && (isolationLevel.trim().length() > 0)) {
dbcpProperties.put("defaultTransactionIsolation", isolationLevel);
}
// Turn off autocommit (unless autocommit property is set)
// Note that this property will be overwritten below if the DBCP "defaultAutoCommit" property is defined.
String autocommit = props.getProperty(AUTOCOMMIT);
if ((autocommit != null) && (autocommit.trim().length() > 0)) {
dbcpProperties.put("defaultAutoCommit", autocommit);
} else {
dbcpProperties.put("defaultAutoCommit", String.valueOf(Boolean.FALSE));
}
// Pool size
String poolSize = props.getProperty(Environment.POOL_SIZE);
if ((poolSize != null) && (poolSize.trim().length() > 0) && (Integer.parseInt(poolSize) > 0)) {
dbcpProperties.put("maxTotal", poolSize);
}
// Copy all "driver" properties into "connectionProperties"
Properties driverProps = ConnectionProviderFactory.getConnectionProperties(props);
if (driverProps.size() > 0) {
StringBuilder connectionProperties = new StringBuilder();
for (Iterator iter = driverProps.keySet().iterator(); iter.hasNext();) {
String key = (String) iter.next();
String value = driverProps.getProperty(key);
connectionProperties.append(key).append('=').append(value);
if (iter.hasNext()) {
connectionProperties.append(';');
}
}
dbcpProperties.put("connectionProperties", connectionProperties.toString());
}
// Copy all DBCP properties removing the prefix
for (Object element : props.keySet()) {
String key = String.valueOf(element);
if (key.startsWith(PREFIX)) {
String property = key.substring(PREFIX.length());
String value = props.getProperty(key);
// Handle backward compatibility
switch (property) {
case COMPATIBILITY_PS_MAXACTIVE:
dbcpProperties.put("poolPreparedStatements", String.valueOf(Boolean.TRUE));
dbcpProperties.put("maxOpenPreparedStatements", value);
break;
case COMPATIBILITY_MAXACTIVE:
dbcpProperties.put("maxTotal", value);
break;
case COMPATIBILITY_MAXWAIT:
dbcpProperties.put("maxWaitMillis", value);
break;
default:
dbcpProperties.put(property, value);
}
}
}
// Some debug info
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Creating a DBCP BasicDataSource with the following DBCP factory properties:");
StringWriter sw = new StringWriter();
dbcpProperties.list(new PrintWriter(sw, true));
LOGGER.debug(sw.toString());
}
// Let the factory create the pool
this.ds = BasicDataSourceFactory.createDataSource(dbcpProperties);
// The BasicDataSource has lazy initialization
// borrowing a connection will start the DataSource
// and make sure it is configured correctly.
Connection conn = this.ds.getConnection();
conn.close();
// Log pool statistics before continuing.
logStatistics();
} catch (Exception e) {
String message = "Could not create a DBCP pool. There is an error in the Hibernate configuration file, "
+ "please review it.";
LOGGER.error(message, e);
if (this.ds != null) {
try {
this.ds.close();
} catch (Exception e2) {
// ignore
}
this.ds = null;
}
throw new HibernateException(message, e);
}
LOGGER.debug("Configure DBCPConnectionProvider complete");
}
@Override
public Connection getConnection() throws SQLException
{
// Check if the database has not been already stopped to avoid NPE. This could also possibly happen if the init
// has not been already executed and some code calls getConnection().
if (this.ds == null) {
throw new SQLException("Database Connection Pool has not been started or is already stopped!");
}
Connection conn = null;
try {
conn = this.ds.getConnection();
} finally {
logStatistics();
}
return conn;
}
@Override
public void closeConnection(Connection conn) throws SQLException
{
try {
conn.close();
} finally {
logStatistics();
}
}
@Override
public void close() throws HibernateException
{
SHUTDOWN_LOGGER.debug("Stopping Database Connection Pool...");
logStatistics();
try {
if (this.ds != null) {
this.ds.close();
this.ds = null;
} else {
LOGGER.warn("Cannot close Database Connection Pool (not initialized)");
}
} catch (Exception e) {
throw new HibernateException("Could not close DBCP pool", e);
}
SHUTDOWN_LOGGER.debug("Database Connection Pool has been stopped");
}
/**
* Does this connection provider support aggressive release of JDBC connections and re-acquistion of those
* connections (if need be) later?
* <p>
* This is used in conjunction with {@link org.hibernate.cfg.Environment#RELEASE_CONNECTIONS} to aggressively
* release JDBC connections. However, the configured ConnectionProvider must support re-acquisition of the same
* underlying connection for that semantic to work.
* <p>
* Typically, this is only true in managed environments where a container tracks connections by transaction or
* thread.
*/
@Override
public boolean supportsAggressiveRelease()
{
return false;
}
protected void logStatistics()
{
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("active: [{}] (max: [{}]), idle: [{}] (max: [{}])", this.ds.getNumActive(),
this.ds.getMaxTotal(), this.ds.getNumIdle(), this.ds.getMaxIdle());
}
}
}