This is an automated email from the ASF dual-hosted git repository. rombert pushed a commit to annotated tag org.apache.sling.datasource-1.0.0 in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-datasource.git
commit a836938dd7303864fe0e41841949062286f26ba1 Author: Chetan Mehrotra <[email protected]> AuthorDate: Tue May 20 10:28:07 2014 +0000 SLING-3574 - JDBC DataSource Provider bundle git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/contrib/extensions/datasource@1596184 13f79535-47bb-0310-9956-ffa450edef68 --- README.md | 59 +++++ pom.xml | 279 +++++++++++++++++++++ .../java/org/apache/juli/logging/DirectJDKLog.java | 110 ++++++++ .../datasource/internal/DataSourceFactory.java | 230 +++++++++++++++++ .../datasource/internal/DriverDataSource.java | 204 +++++++++++++++ .../datasource/internal/DriverRegistry.java | 175 +++++++++++++ .../OSGI-INF/metatype/metatype.properties | 47 ++++ .../sling/extensions/datasource/DataSourceIT.java | 77 ++++++ .../extensions/datasource/DataSourceTestBase.java | 117 +++++++++ 9 files changed, 1298 insertions(+) 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; + } +} -- To stop receiving notification emails like this one, please contact "[email protected]" <[email protected]>.
