package com.joe.utilities.core.configuration;
import java.beans.PropertyVetoException;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.naming.ConfigurationException;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import org.apache.commons.configuration.CompositeConfiguration;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.configuration.SystemConfiguration;
import org.apache.commons.dbcp.BasicDataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.joe.utilities.core.util.ReturnStatus;
import com.joe.utilities.core.validation.Validator;
import com.jolbox.bonecp.BoneCPDataSource;
import com.mchange.v2.c3p0.ComboPooledDataSource;
/**
* The DataSourceFactory is responsible for creating or retrieving the DataSource Object to be
* used by all components. object allow us to be more flexible with accessing thewith the
* <br><br>
* <b>DataSource configuration.</b>
* <br><br>
* This Application utilizes a Datasource to obtain connections to the database.
* The datasource must be properly configured for this application to function properly.
* <br><br>
* Ideally, the dataSource will be configured and managed by the application server. Most
* application servers provide some type of pooled connections to the database, usually thru
* a datasource.
* <br><br>
* If you are using an application server that cannot manage connections, you can alternatively
* configure generic jdbc database information to create a dataSource where the pooling is managed
* by the Medecision application.
* <br><br>
* <table border=1>
* <tr><td colspan=2>
* Note: Multiple datasources can be configured, each requiring the following information; where {name}
* represents the name of the datasource as referenced within the application.
* </td></tr>
* <tr><th>Key</th><th>Description</th></tr>
* <tr><td>com.med.config.dataSource.list </td><td>names of the datasources separated by comma (example default, oracle1) <br>One definition Per file</td></tr>
* <tr><td>com.med.config.dataSource.{name}.type </td><td>jndi [for container managed], or jdbc [for application managed].</td></tr>
* <tr><td>com.med.config.dataSource.{name}.jndi.name </td><td>if jndi, the jndi path & name used to retrieve the dataSource from jndi.</td></tr>
* <tr><td>com.med.config.dataSource.{name}.jdbc.driver </td><td>JDBC Driver used to connect to the database.</td></tr>
* <tr><td>com.med.config.dataSource.{name}.jdbc.url </td><td>JDBC URL used to connect to the database.</td></tr>
* <tr><td>com.med.config.dataSource.{name}.jdbc.user </td><td>UserId used to connect to the database.</td></tr>
* <tr><td>com.med.config.dataSource.{name}.jdbc.password </td><td>Password used to connect to the database.</td></tr>
* </table>
* @author rrichard
*
*/
public class DataSourceFactory implements Validator {
/** Contains the default value to use for the c3p0 checkoutTimeout value. */
private static final int C3PO_DEFAULT_CHECKOUT_TIMEOUT = 5000;
private static Log logger = LogFactory.getLog(DataSourceFactory.class);
private Configuration config = null;
private static DataSourceFactory instance = new DataSourceFactory();
private Set<String> configuredDataSourceNames = new HashSet<String>(4);
private Map<String, DataSource> validDataSourceMap = new HashMap<String, DataSource>(4);
private static final String RK_DATASOURCE_LIST = "com.med.config.dataSource.list";
private static final String RK_DATASOURCE_TYPE = "com.med.config.dataSource.{0}.type";
private static final String RK_DATASOURCE_JNDI_NAME = "com.med.config.dataSource.{0}.jndi.name";
private static final String RK_DATASOURCE_JDBC_URL = "com.med.config.dataSource.{0}.jdbc.url";
private static final String RK_DATASOURCE_JDBC_DRIVER = "com.med.config.dataSource.{0}.jdbc.driver";
private static final String RK_DATASOURCE_JDBC_USER = "com.med.config.dataSource.{0}.jdbc.user";
private static final String RK_DATASOURCE_JDBC_PASSWORD = "com.med.config.dataSource.{0}.jdbc.password";
private static final String RK_DATASOURCE_JDBC_INITIAL_POOL_SIZE = "com.med.config.dataSource.{0}.jdbc.initialPoolSize";
private static final String RK_DATASOURCE_JDBC_MAX_POOL_SIZE = "com.med.config.dataSource.{0}.jdbc.maxPoolSize";
private static final String RK_DATASOURCE_JDBC_MAX_CONNECTION_WAIT = "com.med.config.dataSource.{0}.jdbc.maxConnectionWait";
private static final String RK_DATASOURCE_JDBC_POOL_PREPARED_STATEMENTS = "com.med.config.dataSource.{0}.jdbc.poolPreparedStatements";
/**
* @throws ConfigurationException
*
*/
private DataSourceFactory() {
initConfiguration();
loadDataSources();
}
public static DataSourceFactory getInstance() {
if (instance == null) {
instance = new DataSourceFactory();
}
return instance;
}
/**
* Forces DataSourceFactory to refresh its configuration.
*/
public static void clearInstance() {
// Clear data sources
for (Map.Entry<String, DataSource> entry : instance.validDataSourceMap.entrySet())
{
DataSource dataSource = entry.getValue();
if (dataSource instanceof ComboPooledDataSource)
{
ComboPooledDataSource c3p0DataSource = (ComboPooledDataSource) dataSource;
c3p0DataSource.close();
}
else if (dataSource instanceof BasicDataSource)
{
BasicDataSource dbcpDataSource = (BasicDataSource) dataSource;
try
{
dbcpDataSource.close();
}
catch (SQLException e)
{
logger.error("Error closing Apache DBCP connection",e);
}
}
}
// Clear instance
instance = null;
}
/**
* Method initConfiguration. Initializes the factory from configuration information in the globals properties file.
* @throws GlobalConfigurationException
* @return void
*/
private void initConfiguration() throws GlobalConfigurationException
{
CompositeConfiguration cc = new CompositeConfiguration();
cc.addConfiguration(new SystemConfiguration());
// customer global properties may be an XMLPropertiesConfiguration
PropertiesConfiguration propertiesConfiguration = Globals.loadCustomerGlobalProperties();
if (propertiesConfiguration != null) {
cc.addConfiguration(propertiesConfiguration);
this.config = cc;
}
}
/**
* Cycles thru the list of data Sources @see this{@link #RK_DATASOURCE_LIST} from SystemConfig.properties
*
*/
private void loadDataSources()
{
String[] dataSourceNames = this.config.getStringArray(RK_DATASOURCE_LIST);
for (String currentDataSource : dataSourceNames)
{
logger.info("Starting Data Source: " + currentDataSource);
// Update list of configured data sources
configuredDataSourceNames.add(currentDataSource);
DataSource currDS = null;
if ("jndi".equalsIgnoreCase(this.config.getString(MessageFormat.format(RK_DATASOURCE_TYPE, currentDataSource))))
{
currDS = getJNDIDataSource(currentDataSource);
}
else
{
currDS = getJDBCDataSource(currentDataSource);
}
testConnection(currDS);
this.validDataSourceMap.put(currentDataSource, currDS);
logger.info("Data Source: " + currentDataSource + " seems to be up.");
}
}
/** Test a connection to see if it is valid. It uses DatabaseMetaData.getMetaData() to see if
* it can retrieve meta data against the database.
* @param currDS DataSource to test
* @throws GlobalConfigurationException Thrown when the system cannot connect to the database.
*/
public void testConnection(DataSource currDS) {
Connection conn = null;
ResultSet rsSchema = null;
try{
if (currDS == null) {
logger.error(".testConnection: DataSource is null");
throw new GlobalConfigurationException("Invalid DataSource");
}
conn = currDS.getConnection();
if(conn == null) {
logger.error(".testConnection: Connection is null");
throw new GlobalConfigurationException("Unable to Connect to database");
}
DatabaseMetaData dbmd = conn.getMetaData();
rsSchema = dbmd.getSchemas();
} catch(SQLException e) {
logger.error(".testConnection: " + e.getMessage());
throw new GlobalConfigurationException("Unable to Connect to database: " + e.getMessage());
} finally {
if (rsSchema != null ){
try {
rsSchema.close();
} catch (SQLException e) {
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
}
}
}
}
/**
* This Methods creates a Datasource from org.apache.commons.dbcp.BasicDataSource.
* @return Returns the Datasource.
*/
private DataSource getJDBCDataSource(String dataSourceName)
{
// Determine type of JDBC data source.
String setting = MessageFormat.format(RK_DATASOURCE_TYPE, dataSourceName);
String jdbcType = this.config.getString(setting);
if (jdbcType == null)
throw new GlobalConfigurationException("JDBC type not specified for data source. Missing global properties setting: " + setting);
jdbcType = jdbcType.toLowerCase();
// Use default of Apache DBCP if generic "jdbc" is still
if ("jdbc".equals(jdbcType))
jdbcType = "apache-dbcp";
// Setup data source
if (jdbcType.startsWith("apache"))
return getApacheDBCPDataSource(dataSourceName);
else if (jdbcType.startsWith("c3p"))
return getC3P0DataSource(dataSourceName);
else
throw new GlobalConfigurationException("Unexpected JDBC type specified at global properties setting '" + setting + "' supported values include 'apache-dbcp' and 'c3p0'.");
}
/**
* Gets the apache dbcp data source.
*
* @param dataSourceName the data source name
*
* @return the Apache DBCP data source
*/
private DataSource getApacheDBCPDataSource(String dataSourceName)
{
logger.info("Initializing '"+dataSourceName+"' data source using Apache DBCP...");
// Setup Apache DBCP basic data source with mandatory settings
BasicDataSource basicDataSource = new BasicDataSource();
basicDataSource.setDriverClassName(validateAndReturnRequired(MessageFormat.format(RK_DATASOURCE_JDBC_DRIVER, dataSourceName)));
basicDataSource.setUrl(validateAndReturnRequired(MessageFormat.format(RK_DATASOURCE_JDBC_URL, dataSourceName)));
basicDataSource.setUsername(validateAndReturnRequired(MessageFormat.format(RK_DATASOURCE_JDBC_USER, dataSourceName)));
basicDataSource.setPassword(validateAndReturnRequired(MessageFormat.format(RK_DATASOURCE_JDBC_PASSWORD, dataSourceName)));
// Setup optional settings. If not set, then Apache Commons DBCP default settings are used.
String initialPoolSize = MessageFormat.format(RK_DATASOURCE_JDBC_INITIAL_POOL_SIZE, dataSourceName);
if (config.containsKey(initialPoolSize))
basicDataSource.setInitialSize(config.getInt(initialPoolSize));
String maxPoolSize = MessageFormat.format(RK_DATASOURCE_JDBC_MAX_POOL_SIZE, dataSourceName);
if (config.containsKey(maxPoolSize))
basicDataSource.setMaxActive(config.getInt(maxPoolSize));
// Note: Do not use Apache default of (indefinite wait - System hangs)
String maxConnectionWait = MessageFormat.format(RK_DATASOURCE_JDBC_MAX_CONNECTION_WAIT, dataSourceName);
basicDataSource.setMaxWait(config.getInt(maxConnectionWait, 5000));
// Pool/cache prepared statements
String poolPreparedStatements = MessageFormat.format(RK_DATASOURCE_JDBC_POOL_PREPARED_STATEMENTS, dataSourceName);
basicDataSource.setPoolPreparedStatements(config.getBoolean(poolPreparedStatements, true));
return basicDataSource;
}
/**
* Gets the c3 p0 data source.
* See @link http://www.mchange.com/projects/c3p0/index.html#configuration for complete documentation of
* c3p0 settings. Additional properties may also be setup in "c3p0.properties" file at root of classpath.
* Properties set in this method will override properties set in "c3p0.properties" file.
*
* @param dataSourceName the data source name
*
* @return the C3P0 data source
*/
private DataSource getC3P0DataSource(String dataSourceName)
{
logger.info("Initializing '"+dataSourceName+"' data source using C3P0 pooling...");
// Setup primary C3P0 pooled data source
ComboPooledDataSource c3p0PooledDataSource = new ComboPooledDataSource();
String driverClassSetting = MessageFormat.format(RK_DATASOURCE_JDBC_DRIVER, dataSourceName);
String driverClass = validateAndReturnRequired(driverClassSetting);
try
{
c3p0PooledDataSource.setDriverClass(driverClass);
}
catch (PropertyVetoException e)
{
throw new GlobalConfigurationException("DataSourceFactory: C3P0 did not like the driver class '"+driverClass+"' specified in the global properties at '"+driverClass+"'.", e);
}
c3p0PooledDataSource.setJdbcUrl(validateAndReturnRequired(MessageFormat.format(RK_DATASOURCE_JDBC_URL, dataSourceName)));
c3p0PooledDataSource.setUser(validateAndReturnRequired(MessageFormat.format(RK_DATASOURCE_JDBC_USER, dataSourceName)));
c3p0PooledDataSource.setPassword(validateAndReturnRequired(MessageFormat.format(RK_DATASOURCE_JDBC_PASSWORD, dataSourceName)));
// Setup optional settings. If not set, then C3P0 defaults will be used.
String acquireIncrement = MessageFormat.format("com.med.config.dataSource.{0}.c3p0.acquireIncrement", dataSourceName);
if (config.containsKey(acquireIncrement))
c3p0PooledDataSource.setAcquireIncrement(config.getInt(acquireIncrement));
String maxIdleTime = MessageFormat.format("com.med.config.dataSource.{0}.c3p0.maxIdleTime", dataSourceName);
if (config.containsKey(maxIdleTime))
c3p0PooledDataSource.setMaxIdleTime(config.getInt(maxIdleTime));
String initialPoolSize = MessageFormat.format("com.med.config.dataSource.{0}.c3p0.minPoolSize", dataSourceName);
if (config.containsKey(initialPoolSize))
c3p0PooledDataSource.setInitialPoolSize(config.getInt(initialPoolSize));
String maxPoolSize = MessageFormat.format("com.med.config.dataSource.{0}.c3p0.maxPoolSize", dataSourceName);
if (config.containsKey(maxPoolSize))
c3p0PooledDataSource.setMaxPoolSize(config.getInt(maxPoolSize));
String maxConnectionAge = MessageFormat.format("com.med.config.dataSource.{0}.c3p0.maxConnectionAge", dataSourceName);
if (config.containsKey(maxConnectionAge))
c3p0PooledDataSource.setMaxConnectionAge(config.getInt(maxConnectionAge));
String maxIdleTimeExcessConnections = MessageFormat.format("com.med.config.dataSource.{0}.c3p0.maxIdleTimeExcessConnections", dataSourceName);
if (config.containsKey(maxIdleTimeExcessConnections))
c3p0PooledDataSource.setMaxIdleTimeExcessConnections(config.getInt(maxIdleTimeExcessConnections));
String maxStatements = MessageFormat.format("com.med.config.dataSource.{0}.c3p0.maxStatements", dataSourceName);
if (config.containsKey(maxStatements))
c3p0PooledDataSource.setMaxStatements(config.getInt(maxStatements));
String maxStatementsPerConnection = MessageFormat.format("com.med.config.dataSource.{0}.c3p0.maxStatementsPerConnection", dataSourceName);
if (config.containsKey(maxStatementsPerConnection))
c3p0PooledDataSource.setMaxStatementsPerConnection(config.getInt(maxStatementsPerConnection));
String numHelperThreads = MessageFormat.format("com.med.config.dataSource.{0}.c3p0.numHelperThreads", dataSourceName);
if (config.containsKey(numHelperThreads))
c3p0PooledDataSource.setNumHelperThreads(config.getInt(numHelperThreads));
else
c3p0PooledDataSource.setNumHelperThreads(10);
String acquireRetryAttempts = MessageFormat.format("com.med.config.dataSource.{0}.c3p0.acquireRetryAttempts", dataSourceName);
if (config.containsKey(acquireRetryAttempts))
c3p0PooledDataSource.setAcquireRetryAttempts(config.getInt(acquireRetryAttempts));
String acquireRetryDelay = MessageFormat.format("com.med.config.dataSource.{0}.c3p0.acquireRetryDelay", dataSourceName);
if (config.containsKey(acquireRetryDelay))
c3p0PooledDataSource.setAcquireRetryDelay(config.getInt(acquireRetryDelay));
String breakAfterAcquireFailure = MessageFormat.format("com.med.config.dataSource.{0}.c3p0.breakAfterAcquireFailure", dataSourceName);
if (config.containsKey(breakAfterAcquireFailure))
c3p0PooledDataSource.setBreakAfterAcquireFailure(config.getBoolean(breakAfterAcquireFailure));
String checkoutTimeout = MessageFormat.format("com.med.config.dataSource.{0}.c3p0.checkoutTimeout.milliseconds", dataSourceName);
if (config.containsKey(checkoutTimeout)){
c3p0PooledDataSource.setCheckoutTimeout(config.getInt(checkoutTimeout));
if (0 == config.getInt(checkoutTimeout)){
logger.trace("For c3p0 connection setting chekoutTimeout to 0. If the database goes and a call to getConnection() is made then the system will hang.");
}
} else {
c3p0PooledDataSource.setCheckoutTimeout(DataSourceFactory.C3PO_DEFAULT_CHECKOUT_TIMEOUT);
}
return c3p0PooledDataSource;
}
private DataSource getBoneCPDataSource(String dataSourceName){
BoneCPDataSource boneCPDataSource = new BoneCPDataSource();
//boneCPDataSource.set
return boneCPDataSource;
}
/**
* Validate and return required setting.
*
* @param setting the setting
*
* @return the string
*/
private String validateAndReturnRequired(String setting)
{
String value = this.config.getString(setting);
if (value != null)
return value;
else
throw new GlobalConfigurationException("Required globals property setting '"+setting+"' is not defined.");
}
/**
* This Methods looks into the current context and retrieves the specified
* Datasource.
*
* @return Returns the Datasource from the Server Context.
*/
private DataSource getJNDIDataSource(String dataSourceName) {
DataSource dataSource = null;
try
{
String jndiName = config.getString(MessageFormat.format(RK_DATASOURCE_JNDI_NAME, dataSourceName));
logger.info("Initializing '"+dataSourceName+"' data source using JNDI with name '"+jndiName+"'...");
dataSource = (DataSource) new InitialContext().lookup(jndiName);
}
catch (NamingException e) {
throw new GlobalConfigurationException("Unable to Retrieve dataSource from Context["+ dataSourceName + "]", e);
}
return dataSource;
}
/**
* This Method returns a cached DataSource configured at the time DataSourceFactory is
* created. If the datasource could not be created, of the user entered a datasource that
* was never configured, this method throws an exception.
* @param dsName
* @return
* @throws ConfigurationException Thrown if the DataSource could not be found, or if the Datasource
* failed the connection test.
* @see this{@link #testConnection(DataSource)}
*/
public DataSource getDataSource(String dsName)
{
// Data source request is not configured
if (!instance.configuredDataSourceNames.contains(dsName))
throw new GlobalConfigurationException("Invalid DataSource["+ dsName + "]: This data source is not specified in the globals properties configuration or is otherwise not valid.");
DataSource returnValue = instance.validDataSourceMap.get(dsName);
return returnValue;
}
/**
* @see com.med.configuration.validation.Validator#validate()
*/
public ReturnStatus validate()
{
// ReturnStatus status = new ReturnStatus();
CompositeGlobalConfigurationException errorList = null;
for (String dsName : validDataSourceMap.keySet()) {
try {
testConnection(getDataSource(dsName));
} catch ( GlobalConfigurationException error ){
// Add error generated by testConnection
if ( errorList == null ){
errorList = new CompositeGlobalConfigurationException(error);
} else {
errorList.addException(error);
}
// Add error stating the name of the problem data source
errorList.addException( new GlobalConfigurationException("Problem with dataSource [" + dsName +"]"));
}
}
if ( errorList != null ){
throw errorList;
}
// return status;
return new ReturnStatus();
}
}