Permalink
Browse files

Allow classloader to be specified for classloading

Fixes #1152

Modifies the DatabaseConfig.forConfig and friends methods to take a
classloader to be used for classloading, as well as JdbcDataSource.
Unfortunately, JdbcBackend can't be modified without breaking binary
compatibility, since it's a trait.

Binary compatibility has been maintained, and I fixed the build so the
mima report works and is now run by travis.
  • Loading branch information...
jroper committed May 28, 2015
1 parent e9ab330 commit 1abf7539d3efa97103c770d82fc725c134960f3c
View
@@ -1,5 +1,5 @@
language: scala
script: sbt -jvm-opts jvmopts.travis -Dslick.testkit-config=test-dbs/testkit.travis.conf +testAll
script: sbt -jvm-opts jvmopts.travis -Dslick.testkit-config=test-dbs/testkit.travis.conf +testAll mimaReportBinaryIssues
jdk:
- openjdk6
notifications:
View
@@ -203,7 +203,7 @@ object SlickBuild extends Build {
sdlcCheckDir := (target in com.typesafe.sbt.SbtSite.SiteKeys.makeSite).value,
sdlc <<= sdlc dependsOn (doc in Compile, com.typesafe.sbt.SbtSite.SiteKeys.makeSite),
test := (), testOnly := (), // suppress test status output
previousArtifact := Some("com.typesafe.slick" %% "slick" % binaryCompatSlickVersion),
previousArtifact := Some("com.typesafe.slick" % ("slick_" + scalaBinaryVersion.value) % binaryCompatSlickVersion),
binaryIssueFilters ++= Seq(
ProblemFilters.exclude[MissingClassProblem]("slick.util.MacroSupportInterpolationImpl$"),
ProblemFilters.exclude[MissingClassProblem]("slick.util.MacroSupportInterpolationImpl")
@@ -1,5 +1,7 @@
package slick.backend
import slick.util.ClassLoaderUtil
import scala.language.experimental.macros
import java.net.{URL, URI}
@@ -52,10 +54,35 @@ object DatabaseConfig {
* (e.g. in `application.conf` at the root of the class path) if not specified.
*/
def forConfig[P <: BasicProfile : ClassTag](path: String, config: Config = ConfigFactory.load()): DatabaseConfig[P] = {
forConfig(path, config, ClassLoaderUtil.defaultClassLoader)
}
/** Load a driver and database configuration through
* [[https://github.com/typesafehub/config Typesafe Config]].
*
* The following config parameters are available:
* <ul>
* <li>`driver` (String, required): The fully qualified name of a class or object which
* implements the specified profile. If the name ends with `$` it is assumed to be an object
* name, otherwise a class name.</li>
* <li>`db` (Config, optional): The configuration of a database for the driver's backend.
* For JdbcProfile-based' drivers (and thus JdbcBackend), see
* `JdbcBackend.DatabaseFactory.forConfig` for parameters that should be defined inside of
* `db`.</li>
* </ul>
*
* @param path The path in the configuration file for the database configuration (e.g. `foo.bar`
* would find a driver name at config key `foo.bar.driver`) or an empty string
* for the top level of the `Config` object.
* @param config The `Config` object to read from. This defaults to the global app config
* (e.g. in `application.conf` at the root of the class path) if not specified.
* @param classLoader The ClassLoader to use to load any custom classes from.
*/
def forConfig[P <: BasicProfile : ClassTag](path: String, config: Config, classLoader: ClassLoader): DatabaseConfig[P] = {
val n = config.getString((if(path.isEmpty) "" else path + ".") + "driver")
val untypedP = try {
(if(n.endsWith("$")) Class.forName(n).getField("MODULE$").get(null)
else Class.forName(n).newInstance())
if(n.endsWith("$")) classLoader.loadClass(n).getField("MODULE$").get(null)
else classLoader.loadClass(n).newInstance()
} catch { case NonFatal(ex) =>
throw new SlickException(s"""Error getting instance of Slick driver "$n"""", ex)
}
@@ -78,7 +105,14 @@ object DatabaseConfig {
* the root of the class path), otherwise as a path in the configuration located at the URI
* without the fragment, which must be a valid URL. Without a fragment, the whole config object
* is used. */
def forURI[P <: BasicProfile : ClassTag](uri: URI): DatabaseConfig[P] = {
def forURI[P <: BasicProfile : ClassTag](uri: URI): DatabaseConfig[P] = forURI(uri, ClassLoaderUtil.defaultClassLoader)
/** Load a driver and database configuration from the specified URI. If only a fragment name
* is given, it is resolved as a path in the global app config (e.g. in `application.conf` at
* the root of the class path), otherwise as a path in the configuration located at the URI
* without the fragment, which must be a valid URL. Without a fragment, the whole config object
* is used. */
def forURI[P <: BasicProfile : ClassTag](uri: URI, classLoader: ClassLoader): DatabaseConfig[P] = {
val (base, path) = {
val f = uri.getRawFragment
val s = uri.toString
@@ -90,9 +124,14 @@ object DatabaseConfig {
val root =
if(base eq null) ConfigFactory.load()
else ConfigFactory.parseURL(new URL(base)).resolve()
forConfig[P](path, root)
forConfig[P](path, root, classLoader)
}
/** Load a driver and database configuration from the URI specified in a [[StaticDatabaseConfig]]
* annotation in the static scope of the caller. */
def forAnnotation[P <: BasicProfile](classLoader: ClassLoader = ClassLoaderUtil.defaultClassLoader)(implicit ct: ClassTag[P]): DatabaseConfig[P] =
macro StaticDatabaseConfigMacros.getWithClassLoaderImpl[P]
/** Load a driver and database configuration from the URI specified in a [[StaticDatabaseConfig]]
* annotation in the static scope of the caller. */
def forAnnotation[P <: BasicProfile](implicit ct: ClassTag[P]): DatabaseConfig[P] =
@@ -125,4 +164,10 @@ object StaticDatabaseConfigMacros {
val uri = c.Expr[String](Literal(Constant(getURI(c))))
reify(DatabaseConfig.forURI[P](new URI(uri.splice))(ct.splice))
}
def getWithClassLoaderImpl[P <: BasicProfile : c.WeakTypeTag](c: Context)(classLoader: c.Expr[ClassLoader])(ct: c.Expr[ClassTag[P]]): c.Expr[DatabaseConfig[P]] = {
import c.universe._
val uri = c.Expr[String](Literal(Constant(getURI(c))))
reify(DatabaseConfig.forURI[P](new URI(uri.splice), classLoader.splice)(ct.splice))
}
}
@@ -14,7 +14,7 @@ import javax.naming.InitialContext
import slick.dbio._
import slick.backend.{DatabasePublisher, DatabaseComponent, RelationalBackend}
import slick.SlickException
import slick.util.{LogUtil, GlobalConfig, SlickLogger, AsyncExecutor}
import slick.util._
import slick.util.ConfigExtensionMethods._
import org.slf4j.LoggerFactory
@@ -220,7 +220,7 @@ trait JdbcBackend extends RelationalBackend {
* connection pools (in particular, the default [[HikariCPJdbcDataSource]]).
*/
def forConfig(path: String, config: Config = ConfigFactory.load(), driver: Driver = null): Database = {
val source = JdbcDataSource.forConfig(if(path.isEmpty) config else config.getConfig(path), driver, path)
val source = JdbcDataSource.forConfig(if(path.isEmpty) config else config.getConfig(path), driver, path, ClassLoaderUtil.defaultClassLoader)
val executor = AsyncExecutor(path, config.getIntOr("numThreads", 20), config.getIntOr("queueSize", 1000))
forSource(source, executor)
}
@@ -6,6 +6,7 @@ import java.util.concurrent.TimeUnit
import java.sql.{SQLException, DriverManager, Driver, Connection}
import javax.sql.DataSource
import com.typesafe.config.Config
import slick.util.ClassLoaderUtil
import slick.util.ConfigExtensionMethods._
import slick.SlickException
@@ -24,12 +25,16 @@ trait JdbcDataSource extends Closeable {
object JdbcDataSource {
/** Create a JdbcDataSource from a `Config`. See [[JdbcBackend.DatabaseFactoryDef.forConfig]]
* for documentation of the supported configuration parameters. */
def forConfig(c: Config, driver: Driver, name: String): JdbcDataSource = {
def forConfig(c: Config, driver: Driver, name: String): JdbcDataSource = forConfig(c, driver, name, ClassLoaderUtil.defaultClassLoader)
/** Create a JdbcDataSource from a `Config`. See [[JdbcBackend.DatabaseFactoryDef.forConfig]]
* for documentation of the supported configuration parameters. */
def forConfig(c: Config, driver: Driver, name: String, classLoader: ClassLoader): JdbcDataSource = {
val pf: JdbcDataSourceFactory = c.getStringOr("connectionPool", "HikariCP") match {
case "disabled" => DriverJdbcDataSource
case "HikariCP" => HikariCPJdbcDataSource
case name =>
val clazz = Class.forName(name)
val clazz = classLoader.loadClass(name)
clazz.getField("MODULE$").get(clazz).asInstanceOf[JdbcDataSourceFactory]
}
pf.forConfig(c, driver, name)
@@ -3,6 +3,7 @@ package slick.jdbc
import java.net.URI
import com.typesafe.config.ConfigException
import slick.util.ClassLoaderUtil
import scala.concurrent.Await
import scala.concurrent.duration.Duration
@@ -160,7 +161,7 @@ object ActionBasedSQLInterpolation {
val uri = StaticDatabaseConfigMacros.getURI(ctxt)
//TODO The database configuration and connection should be cached for subsequent macro invocations
val dc =
try DatabaseConfig.forURI[JdbcProfile](new URI((uri))) catch {
try DatabaseConfig.forURI[JdbcProfile](new URI(uri), ClassLoaderUtil.defaultClassLoader) catch {
case ex @ (_: ConfigException | _: SlickException) =>
ctxt.abort(ctxt.enclosingPosition, s"""Cannot load @StaticDatabaseConfig("$uri"): ${ex.getMessage}""")
}
@@ -0,0 +1,22 @@
package slick.util
/**
* Utilities for working with classloaders
*/
object ClassLoaderUtil {
/** Get the default classloader to use to dynamically load classes. */
val defaultClassLoader: ClassLoader = {
new ClassLoader(this.getClass.getClassLoader) {
override def loadClass(name: String): Class[_] = {
try {
// Try the context classloader first. But, during macro compilation, it's probably wrong, so fallback to this
// classloader.
Thread.currentThread().getContextClassLoader.loadClass(name)
} catch {
case e: ClassNotFoundException => super.loadClass(name)
}
}
}
}
}

0 comments on commit 1abf753

Please sign in to comment.