/*
* #%L
* Wisdom-Framework
* %%
* Copyright (C) 2013 - 2015 Wisdom Framework
* %%
* 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.
* #L%
*/
package org.wisdom.database.jdbc.impl;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.felix.ipojo.annotations.*;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.jdbc.DataSourceFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wisdom.api.configuration.ApplicationConfiguration;
import org.wisdom.api.configuration.Configuration;
import org.wisdom.database.jdbc.service.DataSources;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.*;
/**
* Created by homada on 4/17/15.
*/
@Component(immediate = true)
@Provides
@Instantiate
public class HikariCPDataSources implements DataSources {
private static final Logger LOGGER = LoggerFactory.getLogger(HikariCPDataSources.class);
public static final String DB_CONFIGURATION_PREFIX = "db";
private final BundleContext context;
private ServiceRegistration<DataSource> registration;
/**
* A boolean indicating if the wisdom server is running in 'dev' mode.
*/
private boolean isDev;
private Map<String, HikariDataSource> sources = new HashMap<>();
private Map<String, DataSourceFactory> drivers = new HashMap<>();
@Requires
private ApplicationConfiguration configuration;
public HikariCPDataSources(BundleContext context) {
this.context = context;
LOGGER.info("HikariCP Pool starting...");
}
/**
* Gets the data source with the given name.
*
* @param database the data source name
* @return the data source with the given name, {@literal null} if none match
*/
@Override
public DataSource getDataSource(String database) {
return sources.get(database);
}
@Override
public DataSource getDataSource() {
return sources.get(DEFAULT_DATASOURCE);
}
/**
* Gets the set of data sources (name -> data source).
* It contains the available data sources only.
*
* @return the map of name -> data source, empty if none.
*/
@Override
public Map<String, DataSource> getDataSources() {
HashMap<String, DataSource> map = new HashMap<>();
for (Map.Entry<String, HikariDataSource> entry : sources.entrySet()) {
if (entry.getValue() != null) {
map.put(entry.getKey(), entry.getValue());
}
}
return map;
}
@Override
public Connection getConnection() {
return getConnection(DEFAULT_DATASOURCE, true);
}
/**
* Gets a connection on the default data source.
*
* @param autocommit enables or disables the auto-commit.
* @return the connection, {@literal null} if the default data source is not configured,
* or if the connection cannot be opened.
*/
@Override
public Connection getConnection(boolean autocommit) {
return getConnection(DEFAULT_DATASOURCE, autocommit);
}
@Override
public Connection getConnection(String database) {
return getConnection(database, true);
}
@Override
public Connection getConnection(String database, boolean autocommit) {
DataSource ds = getDataSource(database);
if (ds == null) {
return null;
}
try {
Connection connection = ds.getConnection();
connection.setAutoCommit(autocommit);
return connection;
} catch (SQLException e) {
LOGGER.error("Cannot open connection on data source '{}", database, e);
return null;
}
}
@Validate
public void onStart() throws SQLException{
Configuration dbConfiguration = configuration.getConfiguration(DB_CONFIGURATION_PREFIX);
this.isDev = configuration.isDev();
// Detect all db configurations and create the data sources.
if (dbConfiguration == null) {
LOGGER.info("No data sources configured from the configuration, exiting the data source manager");
// Remove existing ones
sources.clear();
return;
}
Set<String> names = dbConfiguration.asMap().keySet();
LOGGER.info("{} data source(s) identified from the configuration : {}", names.size(), names);
// Check whether we already have sources
// Lost ones need to be removed
// New ones need to be added
// Remaining one need to be 'reinjected'
if (!sources.isEmpty()) {
// Remove lost ones
for (String k : new HashSet<>(sources.keySet())) {
if (!names.contains(k)) {
// Lost one.
LOGGER.info("The data source {} has been removed from configuration");
sources.remove(k);
} else if (names.contains(k)) {
// Remaining data source, reconfiguration
LOGGER.info("Reconfiguring data source {}", k);
HikariDataSource ds = createDataSource(dbConfiguration.getConfiguration(k), k);
sources.put(k, ds);
}
}
}
for (String name : names) {
if (sources.containsKey(name)) {
// The data source is already existing.
continue;
}
Configuration conf = dbConfiguration.getConfiguration(name);
HikariDataSource datasource = createDataSource(conf, name);
sources.put(name, datasource);
}
// Try to open a connection to each data source.
// Register the data sources as services.
for (Map.Entry<String, HikariDataSource> entry : sources.entrySet()) {
try {
if (entry.getValue() != null) {
register(context, entry.getValue(), entry.getKey());
entry.getValue().getConnection().close();
LOGGER.info("Connection successful to data source '{}'", entry.getKey());
} else {
LOGGER.info("The data source '{}' is pending - no driver available", entry.getKey());
}
} catch (SQLException e) {
LOGGER.error("The data source '{}' is configured but the connection failed", entry.getKey(), e);
}
}
}
@Invalidate
public void onStop() {
// Close all data sources
for (Map.Entry<String, HikariDataSource> entry : sources.entrySet()) {
shutdownPool(entry.getValue());
LOGGER.info("Data source '{}' closed", entry.getKey());
}
}
private HikariDataSource createDataSource(Configuration configuration, String dsName) throws SQLException{
HikariConfig hikariConfig = toHikariConfig(configuration, dsName);
boolean registred = registerDriver(configuration);
if(registred){
final HikariDataSource datasource = new HikariDataSource(hikariConfig);
return datasource;
}
//we don't create datasource without driver
return null;
}
private HikariConfig toHikariConfig(Configuration configuration, String dataSourceName){
HikariConfig hikariConfig = new HikariConfig();
String className = configuration.getWithDefault("dataSourceClassName", null);
if (className == null) {
LOGGER.warn("`dataSourceClassName` not present. Will use `jdbcUrl` instead.");
}
hikariConfig.setDriverClassName(configuration.get("driver"));
boolean populated = Patterns.populate(hikariConfig, configuration.get("url"), isDev);
if (populated) {
LOGGER.debug("Data source metadata ('{}') populated from the given url", dataSourceName);
}
hikariConfig.setJdbcUrl(configuration.get("url"));
hikariConfig.setUsername(configuration.get("user"));
hikariConfig.setPassword(configuration.get("password"));
// Frequently used
hikariConfig.setAutoCommit(configuration.getBooleanWithDefault("autoCommit", true));
hikariConfig.setConnectionTimeout(configuration.getIntegerWithDefault("connectionTimeout", 30000));
hikariConfig.setIdleTimeout(configuration.getIntegerWithDefault("idleTimeout", 1000 * 60 * 10));
hikariConfig.setMaxLifetime(configuration.getLongWithDefault("maxLifetime", 1000 * 60 * 30L));
if(configuration.get("connectionTestQuery") != null){
hikariConfig.setConnectionTestQuery(configuration.get("connectionTestQuery"));
}
hikariConfig.setMinimumIdle(configuration.getIntegerWithDefault("minimumIdle", 10));
hikariConfig.setMaximumPoolSize(configuration.getIntegerWithDefault("maximumPoolSize", 10));
String poolName = configuration.get("poolName");
if(poolName != null){
hikariConfig.setPoolName(configuration.get("poolName"));
}
// Infrequently used
hikariConfig.setInitializationFailFast(configuration.getBooleanWithDefault("initializationFailFast", false));
hikariConfig.setIsolateInternalQueries(configuration.getBooleanWithDefault("isolateInternalQueries", false));
hikariConfig.setAllowPoolSuspension(configuration.getBooleanWithDefault("allowPoolSuspension", false));
hikariConfig.setReadOnly(configuration.getBooleanWithDefault("readOnly", false));
hikariConfig.setRegisterMbeans(configuration.getBooleanWithDefault("registerMbeans", false));
String catalog = configuration.get("catalog");
if(catalog != null){
hikariConfig.setCatalog(configuration.get("catalog"));
}
String initSql = configuration.get("initSQL");
if(initSql !=null){
hikariConfig.setConnectionInitSql(configuration.get("initSQL"));
}
String isolation = getIsolationLevel(dataSourceName,configuration);
hikariConfig.setTransactionIsolation(isolation);
hikariConfig.setValidationTimeout(configuration.getIntegerWithDefault("validationTimeout", 5000));
hikariConfig.setLeakDetectionThreshold(configuration.getIntegerWithDefault("leakDetectionThreshold", 0));
hikariConfig.validate();
return hikariConfig;
}
private String getIsolationLevel(String dsName, Configuration dbConf) {
String isolation = dbConf.getWithDefault("isolation", "READ_COMMITTED");
String isolationLevel = "TRANSACTION_READ_COMMITTED";
switch (isolation.toUpperCase()) {
case "NONE":
isolationLevel = "TRANSACTION_NONE";
break;
case "READ_COMMITTED":
isolationLevel = "TRANSACTION_READ_COMMITTED";
break;
case "READ_UNCOMMITTED":
isolationLevel = "TRANSACTION_READ_UNCOMMITTED";
break;
case "REPEATABLE_READ":
isolationLevel = "TRANSACTION_REPEATABLE_READ";
break;
case "SERIALIZABLE":
isolationLevel = "TRANSACTION_SERIALIZABLE";
break;
default:
LOGGER.error("Unknown transaction isolation : " + isolation + " for " + dsName);
break;
}
return isolationLevel;
}
private boolean registerDriver(Configuration config) throws SQLException {
String driver = config.getWithDefault("driver", null);
if (driver == null) {
LOGGER.error("The data source has not driver classname - 'driverClassName' property not set");
return false;
} else {
Driver instance = getDriver(driver);
if (instance == null) {
// The driver is not available
return false;
}
DriverManager.registerDriver(instance);
return true;
}
}
public synchronized Driver getDriver(String classname) throws SQLException {
DataSourceFactory factory = drivers.get(classname);
if (factory != null) {
return factory.createDriver(null);
} else {
return null;
}
}
@Bind(optional = true, aggregate = true)
public synchronized void bindFactory(DataSourceFactory factory, Map<String, String> properties) {
String driverClassName = properties.get(DataSourceFactory.OSGI_JDBC_DRIVER_CLASS);
drivers.put(driverClassName, factory);
checkPendingDatasource(driverClassName);
}
private void checkPendingDatasource(String driverClassName) {
for (Map.Entry<String, HikariDataSource> entry : sources.entrySet()) {
if(entry.getValue() == null && driverClassName.equals(getRequiredDriver(entry.getKey()))){
Configuration dbConfiguration = configuration.getConfiguration(DB_CONFIGURATION_PREFIX).getConfiguration(entry.getKey());
try {
HikariDataSource ds = createDataSource(dbConfiguration, entry.getKey());
sources.put(entry.getKey(), ds);
if (entry.getValue() != null) {
register(context, entry.getValue(), entry.getKey());
entry.getValue().getConnection().close();
LOGGER.info("Connection successful to data source '{}'", entry.getKey());
} else {
LOGGER.error("The data source '{}' cannot be created, despite the driver just arrives",
entry.getKey());
}
} catch (SQLException e) {
LOGGER.error("The data source '{}' is configured but the connection failed", entry.getKey(), e);
}
}
}
}
private String getRequiredDriver(String datasourceName) {
Configuration dbConfiguration = configuration.getConfiguration(DB_CONFIGURATION_PREFIX);
String driver = dbConfiguration.getConfiguration(datasourceName).get("driver");
return driver;
}
@Unbind
public synchronized void unbindFactory(DataSourceFactory factory, Map<String, String> properties) {
String driverClassName = properties.get(DataSourceFactory.OSGI_JDBC_DRIVER_CLASS);
drivers.remove(driverClassName);
invalidateDataSources(driverClassName);
}
private void invalidateDataSources(String driverClassName) {
for (Map.Entry<String, HikariDataSource> entry : sources.entrySet()) {
HikariDataSource ds = entry.getValue();
if (ds != null && driverClassName.equals(getRequiredDriver(entry.getKey()))) {
// A used driver just left....
//TODO Unregister only the leaving Datasource service ?
unregister();
//TODO remove the entry completely ?
sources.put(entry.getKey(), null);
LOGGER.info("Driver {} left the DataSourceFactory", driverClassName);
}
}
}
private void shutdownPool(HikariDataSource source) {
LOGGER.info("Shutting down connection pool.");
if (source != null) {
source.close();
} else {
LOGGER.error("Cannot close a data source not managed by the manager (not anymore at least) : {}", source);
//throw new IllegalArgumentException("Cannot close a data source not managed by the manager :" + source);
}
}
private void register(BundleContext context, DataSource ds, String dbName) {
Dictionary<String, String> props = new Hashtable<>();
Configuration serviceProperties = configuration.getConfiguration("properties");
if(serviceProperties!=null){
Properties properties = serviceProperties.asProperties();
for(Enumeration<Object> keys = properties.keys(); keys.hasMoreElements(); /* NO-OP */){
String serviceProp = (String) keys.nextElement();
props.put(serviceProp, serviceProperties.getOrDie(serviceProp));
}
}
//
// "name" property value from application.conf will silently override the value from service properties.
//
props.put(DataSources.DATASOURCE_NAME_PROPERTY, dbName);
registration = context.registerService(DataSource.class, ds, props);
}
private void unregister() {
if (registration != null) {
registration.unregister();
registration = null;
}
}
/**
* For testing purpose only. Injects the application configuration.
*
* @param configuration the configuration
* @return the current {@link org.wisdom.database.jdbc.impl.HikariCPDataSources}
*/
public HikariCPDataSources setApplicationConfiguration(ApplicationConfiguration configuration) {
this.configuration = configuration;
return this;
}
}