package ddth.dasp.framework.dbc;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import javax.sql.DataSource;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ddth.dasp.common.DaspGlobal;
import ddth.dasp.common.id.IdGenerator;
import ddth.dasp.common.logging.JdbcConnLogger;
import ddth.dasp.common.osgi.IRequireCleanupService;
import ddth.dasp.framework.utils.TimerUtils;
/**
* Abstract implementation of {@link IJdbcFactory}.
*
* @author NBThanh <btnguyen2k@gmail.com>
*/
public abstract class AbstractJdbcFactory implements IJdbcFactory, IRequireCleanupService {
private final static Logger LOGGER = LoggerFactory.getLogger(AbstractJdbcFactory.class);
/**
* Stores established data-sources as a map of {key:DataSource}.
*/
private Map<String, DataSource> dataSources = new ConcurrentHashMap<String, DataSource>();
/**
* Stores information of established data-sources as a map of
* {key:DataSourceInfo}
*/
private Map<String, DataSourceInfo> dataSourceInfos = new ConcurrentHashMap<String, DataSourceInfo>();
/**
* Stores currently opened connections as a map of {connection:open
* connection info}.
*/
private Map<Connection, OpenConnectionInfo> openedConnections = new HashMap<Connection, OpenConnectionInfo>();
/**
* Defines maximum lifetime (ms) for a connection.
*/
private long maxConnectionLifetime = DbcpInfo.DEFAULT_MAX_CONNECTION_LIFETIME;
/**
* Holds the parent factory. If not <code>null</code>, method calls will be
* first delegated to the parent factory. If the parent factory returns
* <code>null</code>, falls back to factory's methods.
*/
private IJdbcFactory parentFactory;
private String ID = IdGenerator.getInstance(IdGenerator.getMacAddr()).generateId64Hex();
/**
* Gets the parent {@link IJdbcFactory}.
*
* @return parentFactory
*/
public IJdbcFactory getParentFactory() {
return parentFactory;
}
/**
* Sets the parent {@link IJdbcFactory}.
*
* @param parentFactory
*/
public AbstractJdbcFactory setParentFactory(IJdbcFactory parentFactory) {
this.parentFactory = parentFactory;
return this;
}
/**
* Getter for {@link #maxConnectionLifetime}.
*
* @return long
*/
public long getMaxConnectionLifetime() {
return maxConnectionLifetime;
}
/**
* Setter for {@link #maxConnectionLifetime}.
*
* @param maximumConnectionLifetime
*/
public AbstractJdbcFactory setMaxConnectionLifetime(long maximumConnectionLifetime) {
this.maxConnectionLifetime = maximumConnectionLifetime > 0 ? maximumConnectionLifetime
: DbcpInfo.DEFAULT_MAX_CONNECTION_LIFETIME;
return this;
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked")
@Override
public void init() {
synchronized (IJdbcFactory.class) {
Object temp = DaspGlobal.getGlobalVar(IJdbcFactory.GLOBAL_KEY);
if (!(temp instanceof Map)) {
temp = new HashMap<String, IJdbcFactory>();
DaspGlobal.setGlobalVar(IJdbcFactory.GLOBAL_KEY, temp);
}
Map<String, IJdbcFactory> allJdbcFactories = (Map<String, IJdbcFactory>) temp;
allJdbcFactories.put(ID, this);
}
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked")
@Override
public void destroy() {
synchronized (IJdbcFactory.class) {
// remove myself from DaspGlobal variable
Object temp = DaspGlobal.getGlobalVar(IJdbcFactory.GLOBAL_KEY);
if (!(temp instanceof Map)) {
temp = new HashMap<String, IJdbcFactory>();
DaspGlobal.setGlobalVar(IJdbcFactory.GLOBAL_KEY, temp);
}
Map<String, IJdbcFactory> allJdbcFactories = (Map<String, IJdbcFactory>) temp;
allJdbcFactories.remove(ID);
}
Connection[] openConnections = openedConnections.keySet().toArray(new Connection[0]);
for (Connection conn : openConnections) {
// close opened connections
try {
releaseConnection(conn);
} catch (Exception e) {
LOGGER.warn(e.getMessage(), e);
}
}
for (DataSource ds : dataSources.values()) {
// clean up datasources
try {
closeDataSource(ds);
} catch (Exception e) {
LOGGER.warn(e.getMessage(), e);
}
}
dataSources.clear();
}
/**
* Clean-up and closes the data source.
*
* @param ds
* @throws SQLException
*/
protected abstract void closeDataSource(DataSource ds) throws SQLException;
/**
* {@inheritDoc}
*/
@Override
public int countOpenConnections() {
synchronized (openedConnections) {
return openedConnections.size();
}
}
/**
* {@inheritDoc}
*/
@Override
public int countDataSources() {
synchronized (dataSources) {
return dataSources.size();
}
}
/**
* Get the validation query of different databases.
*
* @param driverName
* @return
*/
protected String getValidationQuery(String driverName) {
if (driverName.contains("mysql")) {
return "/* ping */ SELECT 1";
}
if (driverName.contains("postgresql") || driverName.contains("sqlserver")) {
return "SELECT 1";
}
if (driverName.contains("oracle")) {
return "SELECT 1 FROM DUAL";
}
return "";
}
/**
* Builds a data-source with default connection pool settings.
*
* @param driver
* @param connUrl
* @param username
* @param password
* @return
* @throws SQLException
*/
protected abstract DataSource buildDataSource(String driver, String connUrl, String username,
String password) throws SQLException;
/**
* Builds a data-source with specified connection pool settings.
*
* @param driver
* @param connUrl
* @param username
* @param password
* @param dbcpInfo
* @return
* @throws SQLException
*/
protected abstract DataSource buildDataSource(String driver, String connUrl, String username,
String password, DbcpInfo dbcpInfo) throws SQLException;
/**
* {@inheritDoc}
*/
@Override
public Connection getConnection(String driver, String connUrl, String username, String password)
throws SQLException {
return getConnection(driver, connUrl, username, password, getMaxConnectionLifetime());
}
/**
* {@inheritDoc}
*/
@Override
public Connection getConnection(String driver, String connUrl, String username,
String password, DbcpInfo dbcpInfo) throws SQLException {
return getConnection(driver, connUrl, username, password, getMaxConnectionLifetime(),
dbcpInfo);
}
/**
* {@inheritDoc}
*/
@Override
public Connection getConnection(String driver, String connUrl, String username,
String password, long maxLifetime) throws SQLException {
return getConnection(driver, connUrl, username, password, maxLifetime, null);
}
/**
* {@inheritDoc}
*/
@Override
public Connection getConnection(String driver, String connUrl, String username,
String password, long maxLifetime, DbcpInfo dbcpInfo) throws SQLException {
if (driver == null || connUrl == null) {
LOGGER.error("Can not get the connection: driver and/or connection url is null!");
return null;
}
/*
* Firstly, gets the connection from parent factory.
*/
Connection conn = parentFactory != null ? parentFactory.getConnection(driver, connUrl,
username, password, maxLifetime, dbcpInfo) : null;
if (conn != null) {
return conn;
}
/*
* Secondly, gets the connection from my ownfactory.
*/
String dsName = calcHash(driver, connUrl, username, password, dbcpInfo);
DataSource ds = getDataSource(driver, connUrl, username, password, dbcpInfo, true);
return getConnectionFromDataSource(dsName, ds, maxLifetime);
}
/**
* {@inheritDoc}
*
* @throws SQLException
*/
@Override
public Connection getConnection(String dataSourceName) throws SQLException {
return getConnection(dataSourceName, getMaxConnectionLifetime());
}
/**
* {@inheritDoc}
*
* @throws SQLException
*/
@Override
public Connection getConnection(String dataSourceName, long maxLifetime) throws SQLException {
if (StringUtils.isBlank(dataSourceName)) {
LOGGER.error("Can not get the connection: datasource name is empty!");
return null;
}
/**
* Firstly, gets the connection from parent factory.
*/
Connection conn = parentFactory != null ? parentFactory.getConnection(dataSourceName,
maxLifetime) : null;
if (conn != null) {
return conn;
}
DataSource ds = dataSources.get(dataSourceName);
return getConnectionFromDataSource(dataSourceName, ds, maxLifetime);
}
/**
* Gets a connection from a specified data-source.
*
* @param dataSourceName
* @param dataSource
* @return
* @throws SQLException
*/
protected Connection getConnectionFromDataSource(String dataSourceName, DataSource dataSource) {
return getConnectionFromDataSource(dataSourceName, dataSource, getMaxConnectionLifetime());
}
protected abstract DataSourceInfo internalGetDataSourceInfo(String name, DataSource ds);
protected DataSourceInfo internalGetDataSourceInfo(String name) {
DataSourceInfo dsInfo = dataSourceInfos.get(name);
if (dsInfo == null) {
dsInfo = new DataSourceInfo(name);
dataSourceInfos.put(name, dsInfo);
}
return dsInfo;
}
/**
* {@inheritDoc}
*/
@Override
public DataSourceInfo getDataSourceInfo(String name) {
DataSource ds = getDataSource(name);
if (ds == null) {
return null;
}
return internalGetDataSourceInfo(name, ds);
}
/**
* {@inheritDoc}
*/
@Override
public DataSourceInfo getDataSourceInfo(String driver, String connUrl, String username,
String password, DbcpInfo dbcpInfo) {
String dsName = calcHash(driver, connUrl, username, password, dbcpInfo);
return getDataSourceInfo(dsName);
}
/**
* Gets a connection from a specified data source.
*
* @param dataSourceName
* String
* @param dataSource
* DataSource
* @param maxConnLifetime
* long maximum connection lifetime in ms
* @return Connection
* @throws SQLException
*/
protected Connection getConnectionFromDataSource(String dataSourceName, DataSource dataSource,
long maxConnLifetime) {
if (dataSource == null) {
return null;
}
try {
Connection conn = dataSource.getConnection();
if (conn != null) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Obtained a connection from datasource [" + dataSourceName
+ "]...");
}
JdbcConnLogger.add(conn);
DataSourceInfo dsInfo = internalGetDataSourceInfo(dataSourceName, dataSource);
if (dsInfo != null) {
dsInfo.incNumOpens();
}
OpenConnectionInfo connInfo = new OpenConnectionInfo(dataSourceName);
synchronized (openedConnections) {
openedConnections.put(conn, connInfo);
}
if (maxConnLifetime > 0) {
TimerTask task = new OpenConnectionGuardTimerTask(conn, connInfo);
TimerUtils.getTimer().schedule(task, maxConnLifetime);
}
}
return conn;
} catch (SQLException e) {
// gracefully handle the exception
LOGGER.error(e.getMessage(), e);
return null;
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean releaseConnection(Connection conn) {
try {
// Try to release the connection from the parent factory first.
if (parentFactory != null && parentFactory.releaseConnection(conn)) {
return true;
}
} catch (SQLException e) {
// this should never happen!
LOGGER.warn("Can not release connection from parent factory!");
}
JdbcConnLogger.remove(conn);
synchronized (openedConnections) {
OpenConnectionInfo info = openedConnections.get(conn);
if (info != null) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Releasing a connection from datasource ["
+ info.getDatasourceKey() + "]..., it has lived [" + info.getLifetime()
+ "] ms");
}
DataSourceInfo dsInfo = internalGetDataSourceInfo(info.getDatasourceKey());
if (dsInfo != null) {
dsInfo.incNumCloses();
}
openedConnections.remove(conn);
try {
conn.close();
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
}
return true;
} else {
LOGGER.warn("Can not find the connection in open connection list!");
return false;
}
}
}
/**
* Calculates a hash string for a JDBC connection.
*/
protected String calcHash(String driver, String connUrl, String username, String password,
DbcpInfo dbcpInfo) {
StringBuilder sb = new StringBuilder();
sb.append(driver != null ? driver : "NULL");
sb.append(".");
sb.append(connUrl != null ? connUrl : "NULL");
sb.append(".");
sb.append(username != null ? username : "NULL");
sb.append(".");
int passwordHashcode = password != null ? password.hashCode() : "NULL".hashCode();
sb.append(passwordHashcode);
sb.append(".");
sb.append(dbcpInfo != null ? dbcpInfo.hashCode() : "NULL".hashCode());
return sb.toString();
}
/**
* Gets current number of opened connections.
*
* @return int
*/
public int getNumOpenedConnections() {
synchronized (openedConnections) {
return openedConnections.size();
}
}
/**
* Gets current number of active datasources.
*
* @return int
*/
public int getNumDataSources() {
synchronized (dataSources) {
return dataSources.size();
}
}
/**
* Gets a {@link OpenConnectionInfo} associated with a connection.
*
* @param conn
* Connection
* @return OpenConnectionInfo
*/
public OpenConnectionInfo getConnectionInfo(Connection conn) {
return openedConnections.get(conn);
}
/**
* {@inheritDoc}
*/
@Override
public DataSource getDataSource(String dsName) {
return dataSources.get(dsName);
}
/**
* {@inheritDoc}
*/
@Override
public DataSource getDataSource(String driver, String connUrl, String username,
String password, DbcpInfo dbcpInfo) {
String dsName = calcHash(driver, connUrl, username, password, dbcpInfo);
return getDataSource(dsName);
}
/**
* {@inheritDoc}
*
* @throws Exception
*/
@Override
public DataSource getDataSource(String driver, String connUrl, String username,
String password, DbcpInfo dbcpInfo, boolean createIfNeeded) throws SQLException {
String dsName = calcHash(driver, connUrl, username, password, dbcpInfo);
if (!createIfNeeded) {
return getDataSource(dsName);
} else {
synchronized (dataSources) {
DataSource ds = getDataSource(dsName);
if (ds == null) {
ds = buildDataSource(driver, connUrl, username, password, dbcpInfo);
if (ds != null) {
dataSources.put(dsName, ds);
}
}
return ds;
}
}
}
/**
* {@inheritDoc}
*/
@Override
public Map<String, DataSource> getDataSources() {
return Collections.unmodifiableMap(dataSources);
}
/**
* This timer task forcibly closes the opened connection if it has been
* occupied for so long.
*
* @author ThanhNB
*/
class OpenConnectionGuardTimerTask extends TimerTask {
private Connection conn;
private OpenConnectionInfo openConnInfo;
public OpenConnectionGuardTimerTask(Connection conn, OpenConnectionInfo openConnInfo) {
this.conn = conn;
this.openConnInfo = openConnInfo;
}
@Override
public void run() {
OpenConnectionInfo currentConnInfo = openedConnections.get(conn);
if (currentConnInfo != null) {
// found the opened connection
if (currentConnInfo.getId() == openConnInfo.getId()) {
DataSourceInfo dsInfo = internalGetDataSourceInfo(openConnInfo
.getDatasourceKey());
if (dsInfo != null) {
dsInfo.incNumLeakCloses();
}
// the connection instance we are holding has been occupied
// until now!
releaseConnection(conn);
}
}
}
}
}