/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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. */ package org.apache.sling.datasource.internal; import java.lang.management.ManagementFactory; import java.sql.SQLException; import java.util.Arrays; import java.util.Collections; import java.util.Dictionary; import java.util.HashSet; import java.util.Hashtable; import java.util.Map; import java.util.Properties; import java.util.Set; import javax.management.InstanceNotFoundException; import javax.management.MBeanServer; import javax.management.ObjectName; import org.apache.felix.scr.annotations.Activate; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.ConfigurationPolicy; import org.apache.felix.scr.annotations.Deactivate; import org.apache.felix.scr.annotations.Modified; import org.apache.felix.scr.annotations.Property; import org.apache.felix.scr.annotations.PropertyOption; import org.apache.felix.scr.annotations.Reference; import org.apache.sling.commons.osgi.PropertiesUtil; import org.apache.tomcat.jdbc.pool.ConnectionPool; import org.apache.tomcat.jdbc.pool.DataSource; import org.apache.tomcat.jdbc.pool.PoolConfiguration; import org.apache.tomcat.jdbc.pool.PoolProperties; import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; import org.osgi.framework.ServiceRegistration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Component( name = DataSourceFactory.NAME, label = "%datasource.component.name", description = "%datasource.component.description", metatype = true, configurationFactory = true, policy = ConfigurationPolicy.REQUIRE ) public class DataSourceFactory { public static final String NAME = "org.apache.sling.datasource.DataSourceFactory"; @Property static final String PROP_DATASOURCE_NAME = "datasource.name"; @Property(value = PROP_DATASOURCE_NAME) static final String PROP_DS_SVC_PROP_NAME = "datasource.svc.prop.name"; @Property static final String PROP_DRIVERCLASSNAME = "driverClassName"; @Property static final String PROP_URL = "url"; @Property static final String PROP_USERNAME = "username"; @Property(passwordValue = "") static final String PROP_PASSWORD = "password"; /** * Value indicating default value should be used. if the value is set to * this value then that value would be treated as null */ static final String DEFAULT_VAL = "default"; @Property(value = DEFAULT_VAL, options = { @PropertyOption(name = "Default", value = DEFAULT_VAL), @PropertyOption(name = "true", value = "true"), @PropertyOption(name = "false", value = "false")}) static final String PROP_DEFAULTAUTOCOMMIT = "defaultAutoCommit"; @Property(value = DEFAULT_VAL, options = { @PropertyOption(name = "Default", value = DEFAULT_VAL), @PropertyOption(name = "true", value = "true"), @PropertyOption(name = "false", value = "false")}) static final String PROP_DEFAULTREADONLY = "defaultReadOnly"; @Property(value = DEFAULT_VAL, options = { @PropertyOption(name = "Default", value = DEFAULT_VAL), @PropertyOption(name = "NONE", value = "NONE"), @PropertyOption(name = "READ_COMMITTED", value = "READ_COMMITTED"), @PropertyOption(name = "READ_UNCOMMITTED", value = "READ_UNCOMMITTED"), @PropertyOption(name = "REPEATABLE_READ", value = "REPEATABLE_READ"), @PropertyOption(name = "SERIALIZABLE", value = "SERIALIZABLE")}) static final String PROP_DEFAULTTRANSACTIONISOLATION = "defaultTransactionIsolation"; @Property static final String PROP_DEFAULTCATALOG = "defaultCatalog"; @Property(intValue = PoolProperties.DEFAULT_MAX_ACTIVE) static final String PROP_MAXACTIVE = "maxActive"; @Property(intValue = PoolProperties.DEFAULT_MAX_ACTIVE) static final String PROP_MAXIDLE = "maxIdle"; //defaults to maxActive @Property(intValue = 10) static final String PROP_MINIDLE = "minIdle"; //defaults to initialSize @Property(intValue = 10) static final String PROP_INITIALSIZE = "initialSize"; @Property(intValue = 30000) static final String PROP_MAXWAIT = "maxWait"; @Property(intValue = 0) static final String PROP_MAXAGE = "maxAge"; @Property(boolValue = false) static final String PROP_TESTONBORROW = "testOnBorrow"; @Property(boolValue = false) static final String PROP_TESTONRETURN = "testOnReturn"; @Property(boolValue = false) static final String PROP_TESTWHILEIDLE = "testWhileIdle"; @Property static final String PROP_VALIDATIONQUERY = "validationQuery"; @Property(intValue = -1) static final String PROP_VALIDATIONQUERY_TIMEOUT = "validationQueryTimeout"; @Property(intValue = 5000) protected static final String PROP_TIMEBETWEENEVICTIONRUNSMILLIS = "timeBetweenEvictionRunsMillis"; @Property(intValue = 60000) protected static final String PROP_MINEVICTABLEIDLETIMEMILLIS = "minEvictableIdleTimeMillis"; @Property protected static final String PROP_CONNECTIONPROPERTIES = "connectionProperties"; @Property protected static final String PROP_INITSQL = "initSQL"; @Property(value = "StatementCache;SlowQueryReport(threshold=10000);ConnectionState") protected static final String PROP_INTERCEPTORS = "jdbcInterceptors"; @Property(intValue = 30000) protected static final String PROP_VALIDATIONINTERVAL = "validationInterval"; @Property(boolValue = true) protected static final String PROP_LOGVALIDATIONERRORS = "logValidationErrors"; @Property(value = {}, cardinality = 1024) static final String PROP_DATASOURCE_SVC_PROPS = "datasource.svc.properties"; /** * Property names where we need to treat value 'default' as null */ private static final Set<String> PROPS_WITH_DFEAULT = Collections.unmodifiableSet(new HashSet<String>(Arrays.asList( PROP_DEFAULTAUTOCOMMIT, PROP_DEFAULTREADONLY, PROP_DEFAULTTRANSACTIONISOLATION ))); private final Logger log = LoggerFactory.getLogger(getClass()); @Reference private DriverRegistry driverRegistry; private String name; private String svcPropName; private ObjectName jmxName; private ServiceRegistration dsRegistration; private DataSource dataSource; private BundleContext bundleContext; @Activate protected void activate(BundleContext bundleContext, Map<String, ?> config) throws Exception { this.bundleContext = bundleContext; name = getDataSourceName(config); checkArgument(name != null, "DataSource name must be specified via [%s] property", PROP_DATASOURCE_NAME); dataSource = new LazyJmxRegisteringDataSource(createPoolConfig(config)); svcPropName = getSvcPropName(config); registerDataSource(svcPropName); log.info("Created DataSource [{}] with properties {}", name, dataSource.getPoolProperties().toString()); } @Modified protected void modified(Map<String, ?> config) throws Exception { String name = getDataSourceName(config); String svcPropName = getSvcPropName(config); if (!this.name.equals(name) || !this.svcPropName.equals(svcPropName)) { log.info("Change in datasource name/service property name detected. DataSource would be recreated"); deactivate(); activate(bundleContext, config); return; } //Other modifications can be applied at runtime itself //Tomcat Connection Pool is decoupled from DataSource so can be closed and reset dataSource.setPoolProperties(createPoolConfig(config)); closeConnectionPool(); dataSource.createPool(); log.info("Updated DataSource [{}] with properties {}", name, dataSource.getPoolProperties().toString()); } @Deactivate protected void deactivate() { if (dsRegistration != null) { dsRegistration.unregister(); dsRegistration = null; } closeConnectionPool(); dataSource = null; } private void closeConnectionPool() { unregisterJmx(); dataSource.close(); } private PoolConfiguration createPoolConfig(Map<String, ?> config) { Properties props = new Properties(); //Copy the other properties first Map<String, String> otherProps = PropertiesUtil.toMap(config.get(PROP_DATASOURCE_SVC_PROPS), new String[0]); for (Map.Entry<String, String> e : otherProps.entrySet()) { set(e.getKey(), e.getValue(), props); } props.setProperty(org.apache.tomcat.jdbc.pool.DataSourceFactory.OBJECT_NAME, name); for (String propName : DummyDataSourceFactory.getPropertyNames()) { String value = PropertiesUtil.toString(config.get(propName), null); set(propName, value, props); } //Specify the DataSource such that connection creation logic is handled //by us where we take care of OSGi env PoolConfiguration poolProperties = org.apache.tomcat.jdbc.pool.DataSourceFactory.parsePoolProperties(props); poolProperties.setDataSource(createDriverDataSource(poolProperties)); return poolProperties; } private DriverDataSource createDriverDataSource(PoolConfiguration poolProperties) { return new DriverDataSource(poolProperties, driverRegistry, bundleContext, this); } private void registerDataSource(String svcPropName) { Dictionary<String, Object> svcProps = new Hashtable<String, Object>(); svcProps.put(svcPropName, name); svcProps.put(Constants.SERVICE_VENDOR, "Apache Software Foundation"); svcProps.put(Constants.SERVICE_DESCRIPTION, "DataSource service based on Tomcat JDBC"); dsRegistration = bundleContext.registerService(javax.sql.DataSource.class, dataSource, svcProps); } private void registerJmx(ConnectionPool pool) throws SQLException { org.apache.tomcat.jdbc.pool.jmx.ConnectionPool jmxPool = pool.getJmxPool(); if (jmxPool == null) { //jmx not enabled return; } Hashtable<String, String> table = new Hashtable<String, String>(); table.put("type", "ConnectionPool"); table.put("class", javax.sql.DataSource.class.getName()); table.put("name", ObjectName.quote(name)); try { jmxName = new ObjectName("org.apache.sling", table); MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); mbs.registerMBean(jmxPool, jmxName); } catch (Exception e) { log.warn("Error occurred while registering the JMX Bean for " + "connection pool with name {}", jmxName, e); } } ConnectionPool getPool() { return dataSource.getPool(); } private void unregisterJmx() { try { if (jmxName != null) { MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); mbs.unregisterMBean(jmxName); } } catch (InstanceNotFoundException ignore) { // NOOP } catch (Exception e) { log.error("Unable to unregister JDBC pool with JMX", e); } } //~----------------------------------------< Config Handling > static String getDataSourceName(Map<String, ?> config) { return PropertiesUtil.toString(config.get(PROP_DATASOURCE_NAME), null); } static String getSvcPropName(Map<String, ?> config) { return PropertiesUtil.toString(config.get(PROP_DS_SVC_PROP_NAME), PROP_DATASOURCE_NAME); } private static void set(String name, String value, Properties props) { if (PROPS_WITH_DFEAULT.contains(name) && DEFAULT_VAL.equals(value)) { value = null; } if (value != null) { value = value.trim(); } if (value != null && !value.isEmpty()) { props.setProperty(name, value); } } static void checkArgument(boolean expression, String errorMessageTemplate, Object... errorMessageArgs) { if (!expression) { throw new IllegalArgumentException( String.format(errorMessageTemplate, errorMessageArgs)); } } private class LazyJmxRegisteringDataSource extends org.apache.tomcat.jdbc.pool.DataSource { private volatile boolean initialized; public LazyJmxRegisteringDataSource(PoolConfiguration poolProperties) { super(poolProperties); } @Override public ConnectionPool createPool() throws SQLException { ConnectionPool pool = super.createPool(); registerJmxLazily(pool); return pool; } @Override public void close() { initialized = false; super.close(); } private void registerJmxLazily(ConnectionPool pool) throws SQLException { if (!initialized) { synchronized (this) { if (initialized) { return; } DataSourceFactory.this.registerJmx(pool); initialized = true; } } } } /** * Dummy impl to enable access to protected fields */ private static class DummyDataSourceFactory extends org.apache.tomcat.jdbc.pool.DataSourceFactory { static String[] getPropertyNames() { return ALL_PROPERTIES; } } }