Skip to content

Commit

Permalink
Made SlickDDLPlugin much simpler and more robust.
Browse files Browse the repository at this point in the history
Warning: This is a source incompatible change.

Improvements:
* more flexible and robust because it communicates with the user app via the play.api.db.slick.AutoDDL singleton object instead of automagical table object discovery
* safer: scalac checks imports and objects referenced in play.api.db.slick.AutoDDL (unlike the existence of a package mentioned in config like it was before)
* less reflection: now only happens in def fetchAutoDDLobject and just a bit
* does not override user written evolution scripts, but shows error message instead
  • Loading branch information
cvogt committed Jun 28, 2013
1 parent 1e68f79 commit efb6726
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 185 deletions.
7 changes: 7 additions & 0 deletions samples/computer-database/app/models/AutoDDL.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package play.api.db.slick
object AutoDDL extends AutoDDLInterface{
import models._
def tables = Map(
"default" -> Seq(Companies,Computers)
)
}
2 changes: 1 addition & 1 deletion samples/computer-database/conf/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ logger.play=INFO
# Logger provided to your application:
logger.application=DEBUG

slick.default="models.*"
slick.autoddl_dbs=default

slick {
execution-context {
Expand Down
8 changes: 8 additions & 0 deletions samples/play-slick-cake-sample/app/models/AutoDDL.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package play.api.db.slick
object AutoDDL extends AutoDDLInterface{
import models.current.dao._

def tables = Map(
"default" -> Seq(Cats)
)
}
9 changes: 1 addition & 8 deletions samples/play-slick-cake-sample/conf/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,7 @@ db.specific.password=""

# Slick Evolutions
# ~~~~~
slick.default="models.current.dao.*"


# Evolutions
# ~~~~~
# You can disable evolutions if needed
# evolutionplugin=disabled

slick.autoddl_dbs=default

# Logger
# ~~~~~
Expand Down
7 changes: 7 additions & 0 deletions samples/play-slick-sample/app/models/AutoDDL.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package play.api.db.slick
object AutoDDL extends AutoDDLInterface{
import models._
def tables = Map(
"default" -> Seq(Cats)
)
}
8 changes: 1 addition & 7 deletions samples/play-slick-sample/conf/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,7 @@ db.default.password=""

# Slick Evolutions
# ~~~~~
slick.default="models.*"

# Evolutions
# ~~~~~
# You can disable evolutions if needed
# evolutionplugin=disabled

slick.autoddl_dbs=default

# Logger
# ~~~~~
Expand Down
232 changes: 63 additions & 169 deletions src/main/scala/play/api/db/slick/SlickPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,125 +10,87 @@ import play.api.Mode
import scala.slick.lifted.DDL
import play.api.PlayException

object ReflectionUtils {
import annotation.tailrec
import scala.reflect.runtime.universe
import scala.reflect.runtime.universe._

def splitIdentifiers(names: String) = names.split("""\.""").filter(!_.trim.isEmpty).toList
def assembleIdentifiers(ids: List[String]) = ids.mkString(".")

def findFirstModule(names: String)(implicit mirror: JavaMirror): Option[ModuleSymbol] = {
val elems = splitIdentifiers(names)
var i = 1 //FIXME: vars...
var res: Option[ModuleSymbol] = None
while (i < (elems.size + 1) && !res.isDefined) {
try {
res = Some(mirror.staticModule(assembleIdentifiers(elems.slice(0, i))))
} catch {
case e: reflect.internal.MissingRequirementError =>
//FIXME: must be another way to check if a static modules exists than exceptions!?!
} finally {
i += 1
}
}
res
}

def reflectModuleOrField(name: String, base: Any, baseSymbol: Symbol)(implicit mirror: JavaMirror) = {
val baseIM = mirror.reflect(base)
val baseMember = baseSymbol.typeSignature.member(newTermName(name))
val instance = if (baseMember.isModule) {
if (baseMember.isStatic) {
mirror.reflectModule(baseMember.asModule).instance
}
else {
baseIM.reflectModule(baseMember.asModule).instance
}
} else {
assert(baseMember.isTerm, "Expected " + baseMember + " to be something that can be reflected on " + base + " as a field")
baseIM.reflectField(baseMember.asTerm).get
}
instance -> baseMember
}

def scanModuleOrFieldByReflection(instance: Any, sym: Symbol)(checkSymbol: Symbol => Boolean)(implicit mirror: JavaMirror): List[(Any, Symbol)] = {
@tailrec def scanModuleOrFieldByReflection(found: List[(Any, Symbol)],
checked: Vector[Symbol],
instancesNsyms: List[(Any, Symbol)]): List[(Any, Symbol)] = {

val extractMembers: PartialFunction[(Any, Symbol), Iterable[(Any, Symbol)]] = {
case (baseInstance, baseSym) =>
if (baseInstance != null) {
baseSym.typeSignature.members.filter(s => s.isModule || (s.isTerm && s.asTerm.isVal)).map { mSym =>
reflectModuleOrField(mSym.name.decoded, baseInstance, baseSym)
}
} else List.empty
}
val matching = instancesNsyms.flatMap(extractMembers).filter { case (_, s) => checkSymbol(s) }
val candidates = instancesNsyms.flatMap(extractMembers).filter { case (_, s) => !checkSymbol(s) && !checked.contains(s) }
if (candidates.isEmpty)
(found ++ matching).distinct
else
scanModuleOrFieldByReflection(found ++ matching, checked ++ (matching ++ candidates).map(_._2), candidates)
}

scanModuleOrFieldByReflection(List.empty, Vector.empty, List(instance -> sym))
}

trait AutoDDLInterface{
/** A map from play datasource name to slick table objects, for which an evolution with DDL statements should be autogenerated.
*/
def tables : Map[String,Seq[slick.driver.BasicTableComponent#Table[_]]]
}

class SlickDDLPlugin(app: Application) extends Plugin {
private val configKey = "slick"

private def isDisabled: Boolean = app.configuration.getString("evolutionplugin").map(_ == "disabled").headOption.getOrElse(false)

override def enabled = !isDisabled

class SlickDDLException(val message: String) extends Exception(message)
private val CreatedBy = "# --- Created by Slick DDL"
private val configKey = "slick.autoddl_dbs"
def confError(msg:String) = app.configuration.reportError(configKey, msg)

override def onStart(): Unit = {
val conf = app.configuration.getConfig(configKey)
conf.foreach { conf =>
conf.keys.foreach { key =>
val packageNames = conf.getString(key).getOrElse(throw conf.reportError(key, "Expected key " + key + " but could not get its values!", None)).split(",").toSet
if (app.mode != Mode.Prod) {
val evolutionsEnabled = !"disabled".equals(app.configuration.getString("evolutionplugin"))
if (evolutionsEnabled) {
val evolutions = app.getFile("conf/evolutions/" + key + "/1.sql");
if (!evolutions.exists() || Files.readFile(evolutions).startsWith(CreatedBy)) {
try {
evolutionScript(packageNames).foreach { evolutionScript =>
Files.createDirectory(app.getFile("conf/evolutions/" + key));
Files.writeFileIfChanged(evolutions, evolutionScript);
}
} catch {
case e: SlickDDLException => throw conf.reportError(key, e.message, Some(e))
}
app.configuration
.getString(configKey)
.map( _.split(",").map(_.trim).filter(_ != "") ) // remove whitespace and empty db names
.foreach{
_.foreach{ db =>
if (app.mode != Mode.Prod) {
val dir = "conf/evolutions/" + db
val fileName = dir + "/1.sql"
val file = app.getFile(fileName);
if( file.exists() && !Files.readFile( file ).startsWith(CreatedBy) ){
throw confError(
s"File '$fileName' already exists and was not created by SlickDDLPlugin."
+s" Please delete file or remove datasource '$db' from configuration for"
+s" $configKey"
)
}
evolutionScript(db).foreach { script =>
Files.createDirectory(app.getFile(dir));
Files.writeFileIfChanged(file, script);
}
}
}
}
}
}
/** Load AutoDDL object from user Play project using reflection
*/
def fetchAutoDDLobject = {
val mirror = scala.reflect.runtime.universe.runtimeMirror( app.classloader )
val instance =
try{
mirror.reflectModule(
mirror.staticModule("play.api.db.slick.AutoDDL")
).instance
} catch { case e: reflect.internal.MissingRequirementError =>
throw confError(
"Could not find singleton object AutoDDL in package play.api.db.slick."
+" In order to use Play-Slick's AutoDDL feature you need to define"
+" it extending play.api.db.slick.AutoDDLInterface ."
)
}

private val CreatedBy = "# --- Created by "

private val WildcardPattern = """(.*)\.\*""".r

def evolutionScript(names: Set[String]): Option[String] = {
val classloader = app.classloader

import scala.collection.JavaConverters._
try{
instance.asInstanceOf[AutoDDLInterface]
} catch { case e: ClassCastException =>
throw confError(
"Found singleton object AutoDDL in package play.api.db.slick,"
+" but it does not extend play.api.db.slick.AutoDDLInterface, which"
+" is required."
)
}
}

val ddls = reflectAllDDLMethods(names, classloader)
/** generates DDL for given datasource */
def evolutionScript(db:String): Option[String] = {
val ddls =
fetchAutoDDLobject
.tables
.get(db)
.map(_.map(_.ddl))
.getOrElse{
throw confError(s"play.api.db.slick.AutoDDL.tables did not contain datasource '$db'")
}

val delimiter = ";" //TODO: figure this out by asking the db or have a configuration setting?

if (ddls.nonEmpty) {
val ddl = ddls.reduceLeft(_ ++ _)

Some(CreatedBy + "Slick DDL\n" +
Some(CreatedBy+ "\n" +
"# To stop Slick DDL generation, remove this comment and start using Evolutions\n" +
"\n" +
"# --- !Ups\n\n" +
Expand All @@ -139,72 +101,4 @@ class SlickDDLPlugin(app: Application) extends Plugin {
"\n")
} else None
}

def reflectAllDDLMethods(names: Set[String], classloader: ClassLoader): Seq[DDL] = {
import scala.reflect.runtime.universe
import scala.reflect.runtime.universe._

implicit val mirror = universe.runtimeMirror(classloader)

val tableType = typeOf[slick.driver.BasicTableComponent#Table[_]]
def isTable(sym: Symbol) = {
sym.typeSignature.baseClasses.find(_.typeSignature == tableType.typeSymbol.typeSignature).isDefined
}
def tableToDDL(instance: Any) : (java.lang.Class[_],DDL) = {
import scala.language.reflectiveCalls //this is the simplest way to do achieve this, we are using reflection either way...
instance.getClass -> instance.asInstanceOf[{ def ddl: DDL }].ddl
}

val classesAndNames = names.flatMap { name =>
ReflectionUtils.findFirstModule(name) match {
case Some(baseSym) => { //located a module that matches, reflect each module/field in the name then scan for Tables
val baseInstance = mirror.reflectModule(baseSym).instance

val allIds = ReflectionUtils.splitIdentifiers(name.replace(baseSym.fullName, ""))
val isWildCard = allIds.lastOption.map(_ == "*").getOrElse(false)
val ids = if (isWildCard) allIds.init else allIds

val (outerInstance, outerSym) = ids.foldLeft(baseInstance -> (baseSym: Symbol)) {
case ((instance, sym), id) =>
ReflectionUtils.reflectModuleOrField(id, instance, sym)
}

val foundInstances = if (isTable(outerSym) && !isWildCard) {
List(outerInstance)
} else if (isWildCard) {
val instancesNsyms = ReflectionUtils.scanModuleOrFieldByReflection(outerInstance, outerSym)(isTable)
if (instancesNsyms.isEmpty) play.api.Logger.warn("Scanned object: '" + baseSym.fullName + "' for '" + name + "' but did not find any Slick Tables")
instancesNsyms.map(_._1)
} else {
throw new SlickDDLException("Found a matching object: '" + baseSym.fullName + "' for '" + name + "' but it is not a Slick Table and a wildcard was not specified")
}
foundInstances.map { instance => tableToDDL(instance) }
}
case _ => { //no modules located, scan packages..
import scala.collection.JavaConverters._
val classNames = name match {
case WildcardPattern(p) => ReflectionsCache.getReflections(classloader, p) //TODO: would be nicer if we did this using Scala reflection, alas staticPackage is non-deterministic: https://issues.scala-lang.org/browse/SI-6573
.getStore
.get(classOf[TypesScanner])
.keySet.asScala.toSet
case p => Set(p)
}
classNames.flatMap { className =>

val moduleSymbol = try { //FIXME: ideally we should be able to test for existence not use exceptions
Some(mirror.staticModule(className))
} catch {
case e: scala.reflect.internal.MissingRequirementError => None
}

moduleSymbol.filter(isTable).map { moduleSymbol =>
tableToDDL(mirror.reflectModule(moduleSymbol).instance)
}
}
}
}
}

classesAndNames.toSeq.sortBy(_._1.toString).map(_._2).distinct
}
}

0 comments on commit efb6726

Please sign in to comment.