SLING-3574 - JDBC DataSource Provider bundle
git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1596184 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..381e79f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,59 @@
+Apache Sling DataSource Provider
+================================
+
+This bundle enables creating and configuring JDBC DataSource in OSGi environment based on
+OSGi configuration. It uses [Tomcat JDBC Pool][1] as the JDBC Connection Pool provider.
+
+1. Supports configuring the DataSource based on OSGi config wihich rich metatype
+2. Supports deploying of JDBC Driver as independent bundles and not as fragment
+3. Exposes the DataSource stats as JMX MBean
+
+Driver Loading
+--------------
+
+Loading of JDBC driver is tricky on OSGi env. Mostly one has to attach the Driver bundle as a
+fragment bundle to the code which creates the JDBC Connection.
+
+With JDBC 4 onwards the Driver class can be loaded via Java SE Service Provider mechanism (SPM)
+JDBC 4.0 drivers must include the file META-INF/services/java.sql.Driver. This file contains
+the name of the JDBC driver's implementation of java.sql.Driver. For example, to load the JDBC
+driver to connect to a Apache Derby database, the META-INF/services/java.sql.Driver file would
+contain the following entry:
+
+ org.apache.derby.jdbc.EmbeddedDriver
+
+Sling DataSource Provider bundles maintains a `DriverRegistry` which contains mapping of Driver
+bundle to Driver class supported by it. With this feature there is no need to wrap the Driver
+bundle as fragment to DataSource provider bundle
+
+
+Configuration
+-------------
+
+1. Install the current bundle
+2. Install the JDBC Driver bundle
+3. Configure the DataSource from OSGi config for PID `org.apache.sling.extensions.datasource.DataSourceFactory`
+
+If Felix WebConsole is used then you can configure it via Configuration UI at
+http://localhost:8080/system/console/configMgr/org.apache.sling.extensions.datasource.DataSourceFactory
+
+Usage
+-----
+
+Once the required configuration is done the `DataSource` would be registered as part of the OSGi Service Registry
+The service is registered with service property `datasource.name` whose value is the name of datasource provided in
+OSGi config.
+
+Following snippet demonstrates accessing the DataSource named `foo` via DS annotation
+
+ import javax.sql.DataSource;
+ import org.apache.felix.scr.annotations.Reference;
+
+ public class DSExample {
+
+ @Reference(target = "(&(objectclass=javax.sql.DataSource)(datasource.name=foo))")
+ private DataSource dataSource;
+ }
+
+
+[1]: http://tomcat.apache.org/tomcat-7.0-doc/jdbc-pool.html
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..21ed802
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,279 @@
+<?xml version="1.0"?>
+<!--
+/*************************************************************************
+ *
+ * ADOBE CONFIDENTIAL
+ * __________________
+ *
+ * Copyright 2012 Adobe Systems Incorporated
+ * All Rights Reserved.
+ *
+ * NOTICE: All information contained herein is, and remains
+ * the property of Adobe Systems Incorporated and its suppliers,
+ * if any. The intellectual and technical concepts contained
+ * herein are proprietary to Adobe Systems Incorporated and its
+ * suppliers and are protected by trade secret or copyright law.
+ * Dissemination of this information or reproduction of this material
+ * is strictly forbidden unless prior written permission is obtained
+ * from Adobe Systems Incorporated.
+ **************************************************************************/
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>sling</artifactId>
+ <version>18</version>
+ </parent>
+
+ <artifactId>org.apache.sling.extensions.datasource</artifactId>
+ <packaging>bundle</packaging>
+ <version>0.0.1-SNAPSHOT</version>
+
+ <name>Apache Sling DataSource Provider</name>
+ <description>
+ Enables creation of DataSource based on OSGi configuration
+ </description>
+
+ <properties>
+ <sling.java.version>6</sling.java.version>
+ <pax.exam.version>3.4.0</pax.exam.version>
+ <pax.url.version>1.6.0</pax.url.version>
+ <bundle.build.name>
+ ${basedir}/target
+ </bundle.build.name>
+ <bundle.file.name>
+ ${bundle.build.name}/${project.build.finalName}.jar
+ </bundle.file.name>
+ </properties>
+
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <configuration>
+ <instructions>
+ <Embed-Dependency>
+ org.apache.sling.commons.osgi;inline=org/apache/sling/commons/osgi/PropertiesUtil.class,
+ tomcat-jdbc,
+ tomcat-juli
+ </Embed-Dependency>
+ </instructions>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-scr-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.servicemix.tooling</groupId>
+ <artifactId>depends-maven-plugin</artifactId>
+ <version>1.2</version>
+ <executions>
+ <execution>
+ <id>generate-depends-file</id>
+ <goals>
+ <goal>generate-depends-file</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <artifactId>maven-failsafe-plugin</artifactId>
+ <executions>
+ <execution>
+ <goals>
+ <goal>integration-test</goal>
+ <goal>verify</goal>
+ </goals>
+ </execution>
+ </executions>
+ <configuration>
+ <systemPropertyVariables>
+ <project.bundle.file>${bundle.file.name}</project.bundle.file>
+ <coverage.command>${coverage.command}</coverage.command>
+ </systemPropertyVariables>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.core</artifactId>
+ <version>4.3.1</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.compendium</artifactId>
+ <version>4.3.1</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.commons.osgi</artifactId>
+ <version>2.2.0</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.tomcat</groupId>
+ <artifactId>tomcat-jdbc</artifactId>
+ <version>7.0.53</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.tomcat</groupId>
+ <artifactId>tomcat-juli</artifactId>
+ <version>7.0.53</version>
+ </dependency>
+
+ <!-- OSGi test -->
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.apache.felix.framework</artifactId>
+ <version>4.4.0</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.ops4j.pax.exam</groupId>
+ <artifactId>pax-exam-container-forked</artifactId>
+ <version>${pax.exam.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.ops4j.pax.exam</groupId>
+ <artifactId>pax-exam-junit4</artifactId>
+ <version>${pax.exam.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.ops4j.pax.exam</groupId>
+ <artifactId>pax-exam-link-mvn</artifactId>
+ <version>${pax.exam.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.ops4j.pax.url</groupId>
+ <artifactId>pax-url-aether</artifactId>
+ <version>${pax.url.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.ops4j.pax.url</groupId>
+ <artifactId>pax-url-reference</artifactId>
+ <version>${pax.url.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.ops4j.pax.url</groupId>
+ <artifactId>pax-url-wrap</artifactId>
+ <version>${pax.url.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.apache.felix.configadmin</artifactId>
+ <version>1.8.0</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.apache.felix.scr</artifactId>
+ <version>1.8.2</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.h2database</groupId>
+ <artifactId>h2</artifactId>
+ <version>1.4.178</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-simple</artifactId>
+ <version>1.5.2</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>commons-beanutils</groupId>
+ <artifactId>commons-beanutils-core</artifactId>
+ <version>1.8.3</version>
+ </dependency>
+ </dependencies>
+
+ <profiles>
+ <!--
+ copy the package such that IDEs may easily use it without
+ setting the system property
+ -->
+ <profile>
+ <id>ide</id>
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>maven-antrun-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>bundle-file-create</id>
+ <phase>package</phase>
+ <goals>
+ <goal>run</goal>
+ </goals>
+ <configuration>
+ <target>
+ <copy file="${project.build.directory}/${project.build.finalName}.jar" tofile="${project.build.directory}/bundle.jar" />
+ </target>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ <profile>
+ <id>coverage</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.jacoco</groupId>
+ <artifactId>jacoco-maven-plugin</artifactId>
+ <executions>
+ <!-- Default to setup argLine required by surefire -->
+ <execution>
+ <id>prepare-agent-surefire</id>
+ <phase>test-compile</phase>
+ <goals>
+ <goal>prepare-agent</goal>
+ </goals>
+ <configuration>
+ <propertyName>coverage.command</propertyName>
+ <includes>
+ <include>org.apache.sling.extensions.datasource.internal.*</include>
+ </includes>
+ </configuration>
+ </execution>
+ <execution>
+ <id>report</id>
+ <phase>post-integration-test</phase>
+ <goals>
+ <goal>report</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+</project>
diff --git a/src/main/java/org/apache/juli/logging/DirectJDKLog.java b/src/main/java/org/apache/juli/logging/DirectJDKLog.java
new file mode 100644
index 0000000..f49ac08
--- /dev/null
+++ b/src/main/java/org/apache/juli/logging/DirectJDKLog.java
@@ -0,0 +1,110 @@
+/*
+ * 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.juli.logging;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Overriding the DirectJDKLog impl to delegate the logging to Slf4j
+ */
+@SuppressWarnings("UnusedDeclaration")
+class DirectJDKLog implements Log {
+ private final Logger logger;
+
+ public DirectJDKLog(String name) {
+ this.logger = LoggerFactory.getLogger(name);
+ }
+
+ static Log getInstance(String name) {
+ return new DirectJDKLog( name );
+ }
+
+ public boolean isDebugEnabled() {
+ return logger.isDebugEnabled();
+ }
+
+ public boolean isErrorEnabled() {
+ return logger.isErrorEnabled();
+ }
+
+ public boolean isFatalEnabled() {
+ return logger.isErrorEnabled();
+ }
+
+ public boolean isInfoEnabled() {
+ return logger.isInfoEnabled();
+ }
+
+ public boolean isTraceEnabled() {
+ return logger.isTraceEnabled();
+ }
+
+ public boolean isWarnEnabled() {
+ return logger.isWarnEnabled();
+ }
+
+ public void trace(Object message) {
+ logger.trace(String.valueOf(message));
+ }
+
+ public void trace(Object message, Throwable t) {
+ logger.trace(String.valueOf(message), t);
+ }
+
+ public void debug(Object message) {
+ logger.debug(String.valueOf(message));
+ }
+
+ public void debug(Object message, Throwable t) {
+ logger.debug(String.valueOf(message), t);
+ }
+
+ public void info(Object message) {
+ logger.info(String.valueOf(message));
+ }
+
+ public void info(Object message, Throwable t) {
+ logger.info(String.valueOf(message), t);
+ }
+
+ public void warn(Object message) {
+ logger.warn(String.valueOf(message));
+ }
+
+ public void warn(Object message, Throwable t) {
+ logger.warn(String.valueOf(message), t);
+ }
+
+ public void error(Object message) {
+ logger.error(String.valueOf(message));
+ }
+
+ public void error(Object message, Throwable t) {
+ logger.error(String.valueOf(message), t);
+ }
+
+ public void fatal(Object message) {
+ logger.error(String.valueOf(message));
+ }
+
+ public void fatal(Object message, Throwable t) {
+ logger.error(String.valueOf(message), t);
+ }
+}
diff --git a/src/main/java/org/apache/sling/extensions/datasource/internal/DataSourceFactory.java b/src/main/java/org/apache/sling/extensions/datasource/internal/DataSourceFactory.java
new file mode 100644
index 0000000..f653fcc
--- /dev/null
+++ b/src/main/java/org/apache/sling/extensions/datasource/internal/DataSourceFactory.java
@@ -0,0 +1,230 @@
+/*
+ * 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.extensions.datasource.internal;
+
+import java.lang.management.ManagementFactory;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.Properties;
+
+import javax.management.InstanceNotFoundException;
+import javax.management.MBeanServer;
+import javax.management.MalformedObjectNameException;
+import javax.management.ObjectName;
+import javax.sql.DataSource;
+
+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.Property;
+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.DataSourceProxy;
+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.extensions.datasource.DataSourceFactory";
+
+ @Property
+ static final String PROP_DATASOURCE_NAME = "datasource.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";
+
+ @Property(intValue = PoolProperties.DEFAULT_MAX_ACTIVE)
+ static final String PROP_MAXACTIVE = "maxActive";
+
+ @Property(value = {}, cardinality = 1024)
+ static final String PROP_DATASOURCE_SVC_PROPS = "datasource.svc.properties";
+
+ private final Logger log = LoggerFactory.getLogger(getClass());
+
+ @Reference
+ private DriverRegistry driverRegistry;
+
+ private String name;
+
+ private ObjectName jmxName;
+
+ private ServiceRegistration dsRegistration;
+
+ private DataSource dataSource;
+
+ @Activate
+ protected void activate(BundleContext bundleContext, Map<String,?> config) throws Exception {
+ Properties props = new Properties();
+ name = PropertiesUtil.toString(config.get(PROP_DATASOURCE_NAME), null);
+
+ checkArgument(name != null, "DataSource name must be specified via [%s] property", PROP_DATASOURCE_NAME);
+
+ //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()){
+ props.setProperty(e.getKey(), e.getValue());
+ }
+
+ props.setProperty(org.apache.tomcat.jdbc.pool.DataSourceFactory.OBJECT_NAME, name);
+
+ for(String propName : DummyDataSourceFactory.getPropertyNames()){
+ String value = PropertiesUtil.toString(config.get(propName), null);
+ if(value != null){
+ props.setProperty(propName, value);
+ }
+ }
+
+ dataSource = createDataSource(props, bundleContext);
+
+ registerDataSource(bundleContext);
+ registerJmx();
+
+ log.info("Created DataSource [{}] with properties {}", name, getDataSourceDetails());
+ }
+
+ @Deactivate
+ protected void deactivate(){
+ if(dsRegistration != null){
+ dsRegistration.unregister();
+ }
+
+ unregisterJmx();
+
+ if(dataSource instanceof DataSourceProxy){
+ ((DataSourceProxy) dataSource).close();
+ }
+
+ }
+
+ private DataSource createDataSource(Properties props, BundleContext bundleContext) throws Exception {
+ PoolConfiguration poolProperties = org.apache.tomcat.jdbc.pool.DataSourceFactory.parsePoolProperties(props);
+
+ DriverDataSource driverDataSource = new DriverDataSource(poolProperties, driverRegistry, bundleContext);
+
+ //Specify the DataSource such that connection creation logic is handled
+ //by us where we take care of OSGi env
+ poolProperties.setDataSource(driverDataSource);
+
+ org.apache.tomcat.jdbc.pool.DataSource dataSource =
+ new org.apache.tomcat.jdbc.pool.DataSource(poolProperties);
+ //initialise the pool itself
+ ConnectionPool pool = dataSource.createPool();
+ driverDataSource.setJmxPool(pool.getJmxPool());
+
+ // Return the configured DataSource instance
+ return dataSource;
+ }
+
+ private void registerDataSource(BundleContext bundleContext) {
+ Dictionary<String,Object> svcProps = new Hashtable<String, Object>();
+ svcProps.put(PROP_DATASOURCE_NAME, name);
+ svcProps.put(Constants.SERVICE_VENDOR, "Apache Software Foundation");
+ svcProps.put(Constants.SERVICE_DESCRIPTION, "DataSource service based on Tomcat JDBC");
+ dsRegistration = bundleContext.registerService(DataSource.class, dataSource, svcProps);
+ }
+
+ private void registerJmx() throws MalformedObjectNameException {
+ if(dataSource instanceof DataSourceProxy){
+ org.apache.tomcat.jdbc.pool.jmx.ConnectionPool pool =
+ ((DataSourceProxy) dataSource).getPool().getJmxPool();
+
+ if(pool == null){
+ //jmx not enabled
+ return;
+ }
+ Hashtable<String, String> table = new Hashtable<String, String>();
+ table.put("type", "ConnectionPool");
+ table.put("class", DataSource.class.getName());
+ table.put("name", ObjectName.quote(name));
+ jmxName = new ObjectName("org.apache.sling", table);
+
+ try {
+ MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
+ mbs.registerMBean(pool, jmxName);
+ }catch(Exception e){
+ log.warn("Error occurred while registering the JMX Bean for " +
+ "connection pool with name {}",jmxName, e);
+ }
+ }
+ }
+
+ 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);
+ }
+ }
+
+ private String getDataSourceDetails() {
+ if(dataSource instanceof DataSourceProxy){
+ return ((DataSourceProxy) dataSource).getPoolProperties().toString();
+ }
+ return "<UNKNOWN>";
+ }
+
+
+
+ public static void checkArgument(boolean expression,
+ String errorMessageTemplate,
+ Object... errorMessageArgs) {
+ if (!expression) {
+ throw new IllegalArgumentException(
+ String.format(errorMessageTemplate, errorMessageArgs));
+ }
+ }
+
+ /**
+ * 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;
+ }
+ }
+}
diff --git a/src/main/java/org/apache/sling/extensions/datasource/internal/DriverDataSource.java b/src/main/java/org/apache/sling/extensions/datasource/internal/DriverDataSource.java
new file mode 100644
index 0000000..e2e0b32
--- /dev/null
+++ b/src/main/java/org/apache/sling/extensions/datasource/internal/DriverDataSource.java
@@ -0,0 +1,204 @@
+/*
+ * 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.extensions.datasource.internal;
+
+
+import java.io.PrintWriter;
+import java.sql.Connection;
+import java.sql.Driver;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Properties;
+import java.util.logging.Logger;
+
+import javax.sql.DataSource;
+
+import org.apache.tomcat.jdbc.pool.ConnectionPool;
+import org.apache.tomcat.jdbc.pool.PoolConfiguration;
+import org.apache.tomcat.jdbc.pool.PoolUtilities;
+import org.osgi.framework.BundleContext;
+import org.slf4j.LoggerFactory;
+
+/**
+ * DataSource implementation which only implements the Connection creation part. Tomcat
+ * JDBC currently does not support specifying the Drive instance directly. While running
+ * in OSGi env DriverRegistry maintains a list of seen driver instances.
+ *
+ * DriverDataSource make use of the DriverRegistry to lookup right Driver instance. This avoid
+ * the requirement of having the Driver OSGi bundle attaches as fragments to our bundle
+ */
+class DriverDataSource implements DataSource {
+ private final PoolConfiguration poolProperties;
+ private final DriverRegistry driverRegistry;
+ private final BundleContext bundleContext;
+ private final org.slf4j.Logger log = LoggerFactory.getLogger(getClass());
+ private org.apache.tomcat.jdbc.pool.jmx.ConnectionPool jmxPool;
+ private Driver driver;
+
+ public DriverDataSource(PoolConfiguration poolProperties, DriverRegistry driverRegistry,
+ BundleContext bundleContext) {
+ this.poolProperties = poolProperties;
+ this.driverRegistry = driverRegistry;
+ this.bundleContext = bundleContext;
+ }
+
+ public Connection getConnection() throws SQLException {
+ return getConnection(null, null);
+ }
+
+ public Connection getConnection(String usr, String pwd) throws SQLException {
+ Properties properties = PoolUtilities.clone(poolProperties.getDbProperties());
+ if(usr == null){
+ usr = poolProperties.getUsername();
+ }
+ if(pwd == null){
+ pwd= poolProperties.getPassword();
+ }
+
+ if (usr != null) properties.setProperty(PoolUtilities.PROP_USER, usr);
+ if (pwd != null) properties.setProperty(PoolUtilities.PROP_PASSWORD, pwd);
+
+ String driverURL = poolProperties.getUrl();
+ Connection connection;
+ try {
+ connection = getDriver().connect(driverURL, properties);
+ } catch (Exception x) {
+ if (log.isDebugEnabled()) {
+ log.debug("Unable to connect to database.", x);
+ }
+ //Based on logic in org.apache.tomcat.jdbc.pool.PooledConnection.connectUsingDriver()
+ if (jmxPool!=null) {
+ jmxPool.notify(org.apache.tomcat.jdbc.pool.jmx.ConnectionPool.NOTIFY_CONNECT,
+ ConnectionPool.getStackTrace(x));
+ }
+ if (x instanceof SQLException) {
+ throw (SQLException)x;
+ } else {
+ SQLException ex = new SQLException(x.getMessage());
+ ex.initCause(x);
+ throw ex;
+ }
+ }
+ if (connection==null) {
+ throw new SQLException("Driver:"+driver+" returned null for URL:"+driverURL);
+ }
+
+ return connection;
+ }
+
+ public void setJmxPool(org.apache.tomcat.jdbc.pool.jmx.ConnectionPool jmxPool) {
+ this.jmxPool = jmxPool;
+ }
+
+ //~-------------------------------------< DataSource >
+
+ public PrintWriter getLogWriter() throws SQLException {
+ throw new SQLFeatureNotSupportedException();
+ }
+
+ public void setLogWriter(PrintWriter out) throws SQLException {
+
+ }
+
+ public void setLoginTimeout(int seconds) throws SQLException {
+
+ }
+
+ public int getLoginTimeout() throws SQLException {
+ return 0;
+ }
+
+ public Logger getParentLogger() throws SQLFeatureNotSupportedException {
+ throw new SQLFeatureNotSupportedException();
+ }
+
+ public <T> T unwrap(Class<T> iface) throws SQLException {
+ return null;
+ }
+
+ public boolean isWrapperFor(Class<?> iface) throws SQLException {
+ return false;
+ }
+
+ private Driver getDriver() throws SQLException {
+ if (driver != null) {
+ return driver;
+ }
+ final String url = poolProperties.getUrl();
+
+ Collection<Driver> drivers = driverRegistry.getDrivers();
+ if(!drivers.isEmpty()) {
+ log.debug("Looking for driver for [{}] against registered drivers", url);
+ driver = findMatchingDriver(drivers);
+ }
+
+ if(driver == null){
+ log.debug("Looking for driver for [{}] via provided className [{}]",
+ url, poolProperties.getDriverClassName());
+ driver = loadDriverClass();
+ }
+
+ if(driver == null){
+ //This one is redundant as DriverManager would filter out drivers
+ //whose classes are not visible from our bundle classloader which
+ //means that this list would be empty in most cases
+ log.debug("Looking for driver from DriverManager");
+ driver = findMatchingDriver(Collections.list(DriverManager.getDrivers()));
+ }
+
+ if(driver == null){
+ String msg = String.format("Not able to find any matching driver for url [%s] " +
+ "and driverClassName [%s]",url,poolProperties.getDriverClassName());
+ throw new SQLException(msg);
+ }
+
+ return driver;
+ }
+
+ private Driver loadDriverClass() throws SQLException {
+ try {
+ log.debug("Instantiating driver using class: {} [url={}]",
+ poolProperties.getDriverClassName(),poolProperties.getUrl());
+ return (Driver) bundleContext.getBundle()
+ .loadClass(poolProperties.getDriverClassName()).newInstance();
+ } catch (java.lang.Exception cn) {
+ log.debug("Unable to instantiate JDBC driver.", cn);
+ SQLException ex = new SQLException(cn.getMessage());
+ ex.initCause(cn);
+ throw ex;
+ }
+ }
+
+ private Driver findMatchingDriver(Collection<Driver> drivers) {
+ final String url = poolProperties.getUrl();
+ for (Driver driver : drivers) {
+ try {
+ if (driver.acceptsURL(url)) {
+ return driver;
+ }
+ } catch (SQLException e) {
+ log.debug("Error occurred while matching driver against url {}", url, e);
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/org/apache/sling/extensions/datasource/internal/DriverRegistry.java b/src/main/java/org/apache/sling/extensions/datasource/internal/DriverRegistry.java
new file mode 100644
index 0000000..732433e
--- /dev/null
+++ b/src/main/java/org/apache/sling/extensions/datasource/internal/DriverRegistry.java
@@ -0,0 +1,175 @@
+/*
+ * 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.extensions.datasource.internal;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.sql.Driver;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Service;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleEvent;
+import org.osgi.util.tracker.BundleTracker;
+import org.osgi.util.tracker.BundleTrackerCustomizer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Component
+@Service(value = DriverRegistry.class)
+public class DriverRegistry {
+ private static final String DRIVER_SERVICE = "META-INF/services/"
+ + Driver.class.getName();
+
+ private final Logger log = LoggerFactory.getLogger(getClass());
+
+ private BundleTracker<Collection<DriverInfo>> bundleTracker;
+
+ private ConcurrentMap<DriverInfo, Driver> driverInfos = new ConcurrentHashMap<DriverInfo, Driver>();
+
+ public Collection<Driver> getDrivers() {
+ return driverInfos.values();
+ }
+
+ @Activate
+ protected void activate(BundleContext bundleContext) {
+ bundleTracker = new BundleTracker<Collection<DriverInfo>>(bundleContext,
+ Bundle.ACTIVE, new DriverBundleTracker());
+ bundleTracker.open();
+ }
+
+ @Deactivate
+ protected void deactivate() {
+ if (bundleTracker != null) {
+ bundleTracker.close();
+ }
+ }
+
+ private void registerDrivers(Collection<DriverInfo> drivers) {
+ for (DriverInfo di : drivers) {
+ driverInfos.put(di, di.driver);
+ log.info("Registering {}", di);
+ }
+ }
+
+ private void deregisterDrivers(Collection<DriverInfo> drivers) {
+ for (DriverInfo di : drivers) {
+ driverInfos.remove(di);
+ log.info("Deregistering {}", di);
+ }
+ }
+
+ private Collection<DriverInfo> createDrivers(final Bundle bundle) {
+ URL url = bundle.getEntry(DRIVER_SERVICE);
+ InputStream ins = null;
+ final List<DriverInfo> extensions = new ArrayList<DriverInfo>();
+ try {
+ ins = url.openStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(ins));
+ String line;
+ while ((line = reader.readLine()) != null) {
+ if (!line.startsWith("#") && line.trim().length() > 0) {
+ try {
+ Class<?> clazz = bundle.loadClass(line);
+ extensions.add(new DriverInfo(bundle, (Driver) clazz.newInstance()));
+ } catch (Throwable t) {
+ log.warn("Cannot register java.sql.Driver [{}] from bundle [{}]",
+ new Object[]{line, bundle, t});
+ }
+ }
+ }
+ } catch (IOException ioe) {
+ // ignore
+ } finally {
+ if (ins != null) {
+ try {
+ ins.close();
+ } catch (IOException ignore) {
+ }
+ }
+ }
+
+ return extensions;
+ }
+
+ private class DriverBundleTracker implements BundleTrackerCustomizer<Collection<DriverInfo>> {
+ public Collection<DriverInfo> addingBundle(Bundle bundle, BundleEvent event) {
+ if (bundle.getEntry(DRIVER_SERVICE) != null) {
+ Collection<DriverInfo> drivers = createDrivers(bundle);
+ registerDrivers(drivers);
+ return drivers;
+ }
+ return null;
+ }
+
+ public void modifiedBundle(Bundle bundle, BundleEvent event, Collection<DriverInfo> object) {
+
+ }
+
+ public void removedBundle(Bundle bundle, BundleEvent event, Collection<DriverInfo> drivers) {
+ deregisterDrivers(drivers);
+ }
+ }
+
+ private static class DriverInfo {
+ final Driver driver;
+ final Bundle bundle;
+
+ DriverInfo(Bundle bundle, Driver driver) {
+ this.driver = driver;
+ this.bundle = bundle;
+ }
+
+ @SuppressWarnings("RedundantIfStatement")
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ DriverInfo that = (DriverInfo) o;
+
+ if (!(bundle == that.bundle)) return false;
+ if (!(driver == that.driver)) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = driver.hashCode();
+ result = 31 * result + bundle.hashCode();
+ return result;
+ }
+
+ public String toString() {
+ return String.format("java.sql.Driver [%s] from bundle [%s]", driver.getClass().getName(), bundle);
+ }
+ }
+}
diff --git a/src/main/resources/OSGI-INF/metatype/metatype.properties b/src/main/resources/OSGI-INF/metatype/metatype.properties
new file mode 100644
index 0000000..e0e9e5c
--- /dev/null
+++ b/src/main/resources/OSGI-INF/metatype/metatype.properties
@@ -0,0 +1,47 @@
+#
+# 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.
+#
+
+# suppress inspection "UnusedProperty" for whole file
+
+datasource.component.name = Apache Sling JDBC DataSource
+datasource.component.description = Creates a DataSource services based on configuration provided
+
+datasource.name.name = Datasource name(*)
+datasource.name.description = Name of this data source (required)
+
+datasource.svc.properties.name = Additional Properties
+datasource.svc.properties.description = Properties that are added additionally to the underlying DataSource \
+ provider(name=value pairs). Refer to http://tomcat.apache.org/tomcat-7.0-doc/jdbc-pool.html#Common_Attributes \
+ for various property names and details.
+
+url.name=JDBC connection URI
+url.description=URI of the JDBC connection to use e.g. jdbc:mysql://localhost:3306/mysql
+
+driverClassName.name=JDBC driver class
+driverClassName.description=Java class name of the JDBC driver to use
+
+username.name=Username
+username.description=The connection username to be passed to our JDBC driver to establish a connection
+
+password.name=Password
+password.description=The connection password to be passed to our JDBC driver to establish a connection.
+
+maxActive.name=Max Active Connections
+maxActive.description=The maximum number of active connections that can be allocated from this pool at \
+ the same time. The default value is 100
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/extensions/datasource/DataSourceIT.java b/src/test/java/org/apache/sling/extensions/datasource/DataSourceIT.java
new file mode 100644
index 0000000..08720e2
--- /dev/null
+++ b/src/test/java/org/apache/sling/extensions/datasource/DataSourceIT.java
@@ -0,0 +1,77 @@
+/*
+ * 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.extensions.datasource;
+
+import java.sql.Connection;
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+import javax.inject.Inject;
+import javax.sql.DataSource;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.osgi.framework.Filter;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.util.tracker.ServiceTracker;
+
+import static org.apache.commons.beanutils.BeanUtils.getProperty;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@RunWith(PaxExam.class)
+public class DataSourceIT extends DataSourceTestBase{
+
+ static {
+ //paxRunnerVmOption = DEBUG_VM_OPTION;
+ }
+
+
+ String PID = "org.apache.sling.extensions.datasource.DataSourceFactory";
+
+ @Inject
+ ConfigurationAdmin ca;
+
+ @Test
+ public void testDataSourceAsService() throws Exception{
+ Configuration config = ca.createFactoryConfiguration(PID, null);
+ Dictionary<String, Object> p = new Hashtable<String, Object>();
+ p.put("url","jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");
+ p.put("datasource.name","test");
+ p.put("maxActive",70);
+ config.update(p);
+
+ Filter filter = context.createFilter("(&(objectclass=javax.sql.DataSource)(datasource.name=test))");
+ ServiceTracker<DataSource, DataSource> st =
+ new ServiceTracker<DataSource, DataSource>(context, filter, null);
+ st.open();
+
+ DataSource ds = st.waitForService(10000);
+ assertNotNull(ds);
+
+ Connection conn = ds.getConnection();
+ assertNotNull(conn);
+
+ //Cannot access directly so access via reflection
+ assertEquals("70", getProperty(ds, "poolProperties.maxActive"));
+ }
+
+}
diff --git a/src/test/java/org/apache/sling/extensions/datasource/DataSourceTestBase.java b/src/test/java/org/apache/sling/extensions/datasource/DataSourceTestBase.java
new file mode 100644
index 0000000..7415017
--- /dev/null
+++ b/src/test/java/org/apache/sling/extensions/datasource/DataSourceTestBase.java
@@ -0,0 +1,117 @@
+/*
+ * 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.extensions.datasource;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import javax.inject.Inject;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.FilenameUtils;
+import org.ops4j.pax.exam.Configuration;
+import org.ops4j.pax.exam.CoreOptions;
+import org.ops4j.pax.exam.Option;
+import org.osgi.framework.BundleContext;
+
+import static org.ops4j.pax.exam.CoreOptions.cleanCaches;
+import static org.ops4j.pax.exam.CoreOptions.composite;
+import static org.ops4j.pax.exam.CoreOptions.junitBundles;
+import static org.ops4j.pax.exam.CoreOptions.keepCaches;
+import static org.ops4j.pax.exam.CoreOptions.mavenBundle;
+import static org.ops4j.pax.exam.CoreOptions.options;
+import static org.ops4j.pax.exam.CoreOptions.systemPackage;
+import static org.ops4j.pax.exam.CoreOptions.systemProperty;
+import static org.ops4j.pax.exam.CoreOptions.systemTimeout;
+import static org.ops4j.pax.exam.CoreOptions.workingDirectory;
+import static org.ops4j.pax.exam.CoreOptions.wrappedBundle;
+import static org.ops4j.pax.exam.util.PathUtils.getBaseDir;
+
+public abstract class DataSourceTestBase {
+ @Inject
+ protected BundleContext context;
+
+ // the name of the system property providing the bundle file to be installed
+ // and tested
+ protected static final String BUNDLE_JAR_SYS_PROP = "project.bundle.file";
+
+ // the default bundle jar file name
+ protected static final String BUNDLE_JAR_DEFAULT = "target/bundle.jar";
+
+ // the name of the system property which captures the jococo coverage agent command
+ //if specified then agent would be specified otherwise ignored
+ protected static final String COVERAGE_COMMAND = "coverage.command";
+
+ // the JVM option to set to enable remote debugging
+ @SuppressWarnings("UnusedDeclaration")
+ protected static final String DEBUG_VM_OPTION = "-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=31313";
+
+ // the actual JVM option set, extensions may implement a static
+ // initializer overwriting this value to have the configuration()
+ // method include it when starting the OSGi framework JVM
+ protected static String paxRunnerVmOption = null;
+
+ @Configuration
+ public Option[] config() throws IOException {
+ final String bundleFileName = System.getProperty(BUNDLE_JAR_SYS_PROP, BUNDLE_JAR_DEFAULT);
+ final File bundleFile = new File(bundleFileName);
+ if (!bundleFile.canRead()) {
+ throw new IllegalArgumentException("Cannot read from bundle file " + bundleFileName + " specified in the "
+ + BUNDLE_JAR_SYS_PROP + " system property. Try building the project first " +
+ "with 'mvn clean install -Pide -DskipTests'");
+ }
+ return options(
+ // the current project (the bundle under test)
+ CoreOptions.bundle(bundleFile.toURI().toString()),
+ mavenBundle("com.h2database", "h2").versionAsInProject(),
+ wrappedBundle(mavenBundle("commons-beanutils", "commons-beanutils-core").versionAsInProject()),
+ mavenBundle("org.slf4j", "slf4j-simple").versionAsInProject().noStart(),
+ mavenBundle("org.apache.felix", "org.apache.felix.scr").versionAsInProject(),
+ mavenBundle("org.apache.felix", "org.apache.felix.configadmin").versionAsInProject(),
+ junitBundles(),
+ systemProperty("pax.exam.osgi.unresolved.fail").value("fail"),
+ systemPackage("com.sun.tools.attach"),
+ cleanCaches(),
+ addCodeCoverageOption(),
+ addDebugOptions()
+ );
+ }
+
+ private static Option addCodeCoverageOption() {
+ String coverageCommand = System.getProperty(COVERAGE_COMMAND);
+ if (coverageCommand != null) {
+ return CoreOptions.vmOption(coverageCommand);
+ }
+ return null;
+ }
+
+ private static Option addDebugOptions() throws IOException {
+ if (paxRunnerVmOption != null) {
+ String workDir = FilenameUtils.concat(getBaseDir(), "target/pax");
+ File workDirFile = new File(workDir);
+ if (workDirFile.exists()) {
+ FileUtils.deleteDirectory(workDirFile);
+ }
+ return composite(CoreOptions.vmOption(paxRunnerVmOption), keepCaches(),
+ systemTimeout(TimeUnit.MINUTES.toMillis(10)), workingDirectory(workDir));
+ }
+ return null;
+ }
+}