package play.api.db.evolutions
import java.io.File
import java.nio.file._
import java.nio.charset.Charset
import play.api.{ Application, Configuration, Environment, Logger, Mode, Play }
import play.api.db.{ DBApi, Database }
import play.api.inject.DefaultApplicationLifecycle
import play.api.libs.Codecs.sha1
import play.core.DefaultWebCommands
import play.utils.PlayIO
case class Evolution(revision: Int, sql_up: String = "", sql_down: String = "") {
val hash = sha1(sql_down.trim + sql_up.trim)
}
trait Script {
def evolution: Evolution
def sql: String
def statements: Seq[String] = {
sql.split("(?<!;);(?!;)").map(_.trim.replace(";;", ";")).filter(_ != "")
}
}
case class UpScript(evolution: Evolution) extends Script {
def sql: String = evolution.sql_up
}
case class DownScript(evolution: Evolution) extends Script {
def sql: String = evolution.sql_down
}
private[evolutions] object DatabaseUrlPatterns {
lazy val SqlServerJdbcUrl = "^jdbc:sqlserver:.*".r
lazy val OracleJdbcUrl = "^jdbc:oracle:.*".r
lazy val MysqlJdbcUrl = "^(jdbc:)?mysql:.*".r
}
object Evolutions {
def directoryName(db: String): String = s"conf/evolutions/${db}"
def fileName(db: String, revision: Int): String = s"${directoryName(db)}/${revision}.sql"
def resourceName(db: String, revision: Int): String = s"evolutions/${db}/${revision}.sql"
def applyFor(dbName: String, path: java.io.File = new java.io.File("."), autocommit: Boolean = true): Unit = {
val evolutions = Play.current.injector.instanceOf[EvolutionsApi]
val scripts = evolutions.scripts(dbName, new EnvironmentEvolutionsReader(Environment.simple(path = path)))
evolutions.evolve(dbName, scripts, autocommit)
}
def updateEvolutionScript(db: String = "default", revision: Int = 1, comment: String = "Generated", ups: String, downs: String)(implicit application: Application) {
val environment = application.injector.instanceOf[Environment]
val evolutions = environment.getFile(fileName(db, revision))
Files.createDirectory(environment.getFile(directoryName(db)).toPath)
writeFileIfChanged(evolutions,
"""|# --- %s
|
|# --- !Ups
|%s
|
|# --- !Downs
|%s
|
|""".stripMargin.format(comment, ups, downs))
}
private def writeFileIfChanged(path: File, content: String): Unit = {
if (content != PlayIO.readFileAsString(path)) {
writeFile(path, content)
}
}
private def writeFile(destination: File, content: String): Unit = {
Files.write(destination.toPath, content.getBytes(utf8))
}
private lazy val utf8 = Charset.forName("UTF8")
def toHumanReadableScript(scripts: Seq[Script]): String = {
val txt = scripts.map {
case UpScript(ev) => "# --- Rev:" + ev.revision + ",Ups - " + ev.hash.take(7) + "\n" + ev.sql_up + "\n"
case DownScript(ev) => "# --- Rev:" + ev.revision + ",Downs - " + ev.hash.take(7) + "\n" + ev.sql_down + "\n"
}.mkString("\n")
val hasDownWarning =
"# !!! WARNING! This script contains DOWNS evolutions that are likely destructives\n\n"
if (scripts.exists(_.isInstanceOf[DownScript])) hasDownWarning + txt else txt
}
def conflictings(downs: Seq[Evolution], ups: Seq[Evolution]): (Seq[Evolution], Seq[Evolution]) =
downs.zip(ups).reverse.dropWhile {
case (down, up) => down.hash == up.hash
}.reverse.unzip
def applyEvolutions(database: Database, evolutionsReader: EvolutionsReader = ThisClassLoaderEvolutionsReader,
autocommit: Boolean = true): Unit = {
val dbEvolutions = new DatabaseEvolutions(database)
val evolutions = dbEvolutions.scripts(evolutionsReader)
dbEvolutions.evolve(evolutions, autocommit)
}
def cleanupEvolutions(database: Database, autocommit: Boolean = true): Unit = {
val dbEvolutions = new DatabaseEvolutions(database)
val evolutions = dbEvolutions.resetScripts()
dbEvolutions.evolve(evolutions, autocommit)
}
def withEvolutions[T](database: Database, evolutionsReader: EvolutionsReader = ThisClassLoaderEvolutionsReader,
autocommit: Boolean = true)(block: => T): T = {
applyEvolutions(database, evolutionsReader, autocommit)
try {
block
} finally {
try {
cleanupEvolutions(database, autocommit)
} catch {
case e: Exception =>
Logger.warn("Error resetting evolutions", e)
}
}
}
}
object OfflineEvolutions {
private val logger = Logger(this.getClass)
private def isTest: Boolean = Play.maybeApplication.exists(_.mode == Mode.Test)
private def getEvolutions(appPath: File, classloader: ClassLoader, dbApi: DBApi): EvolutionsComponents = {
val _dbApi = dbApi
new EvolutionsComponents {
lazy val environment = Environment(appPath, classloader, Mode.Dev)
lazy val configuration = Configuration.load(appPath)
lazy val applicationLifecycle = new DefaultApplicationLifecycle
lazy val dynamicEvolutions = new DynamicEvolutions
lazy val dbApi: DBApi = _dbApi
lazy val webCommands = new DefaultWebCommands
}
}
def applyScript(appPath: File, classloader: ClassLoader, dbApi: DBApi, dbName: String, autocommit: Boolean = true): Unit = {
val evolutions = getEvolutions(appPath, classloader, dbApi)
val scripts = evolutions.evolutionsApi.scripts(dbName, evolutions.evolutionsReader)
if (!isTest) {
logger.warn("Applying evolution scripts for database '" + dbName + "':\n\n" + Evolutions.toHumanReadableScript(scripts))
}
evolutions.evolutionsApi.evolve(dbName, scripts, autocommit)
}
def resolve(appPath: File, classloader: ClassLoader, dbApi: DBApi, dbName: String, revision: Int): Unit = {
val evolutions = getEvolutions(appPath, classloader, dbApi)
if (!isTest) {
logger.warn("Resolving evolution [" + revision + "] for database '" + dbName + "'")
}
evolutions.evolutionsApi.resolve(dbName, revision)
}
}