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); } } } } }