Skip to content

Instantly share code, notes, and snippets.

@tamanugi
Created February 27, 2020 15:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tamanugi/f00af56bf870b77ae96dbebea4488dd8 to your computer and use it in GitHub Desktop.
Save tamanugi/f00af56bf870b77ae96dbebea4488dd8 to your computer and use it in GitHub Desktop.
Playframework Custom Evolutions
play.modules {
enabled += "jp.hogehoge.EvolutionsModule"
disabled += "play.api.db.evolutions.EvolutionsModule"
}
import java.io.{ByteArrayOutputStream, Closeable, IOException, InputStream}
import javax.inject._
import play.Logger
import play.api.Environment
import play.api.db.evolutions._
import play.api.inject._
import play.api.libs.Collections
import scala.io.Codec
/**
* Default module for evolutions API.
*/
class EvolutionsModule extends SimpleModule(
bind[EvolutionsConfig].toProvider[DefaultEvolutionsConfigParser],
// Change to Custom Evolition Reader
bind[EvolutionsReader].to[MyEnvironmentEvolutionsReader],
bind[EvolutionsApi].to[DefaultEvolutionsApi],
bind[ApplicationEvolutions].toProvider[ApplicationEvolutionsProvider].eagerly
)
/**
* Read evolution files from the application environment.
*/
@Singleton
class MyEnvironmentEvolutionsReader @Inject() (environment: Environment) extends ResourceEvolutionsReader {
/**
* Not used
*/
def loadResource(db: String, revision: Int): Option[InputStream] = {
Option.empty
}
override def evolutions(db: String): Seq[Evolution] = {
val upsMarker = """^#.*!Ups.*$""".r
val downsMarker = """^#.*!Downs.*$""".r
val UPS = "UPS"
val DOWNS = "DOWNS"
val UNKNOWN = "UNKNOWN"
val mapUpsAndDowns: PartialFunction[String, String] = {
case upsMarker() => UPS
case downsMarker() => DOWNS
case _ => UNKNOWN
}
val isMarker: PartialFunction[String, Boolean] = {
case upsMarker() => true
case downsMarker() => true
case _ => false
}
val folder = environment.getFile(Evolutions.directoryName(db))
val sqlFiles = folder.listFiles()
// filtering with number-prefixed sql
.filter(file => file.getName.matches("^[0-9]+.*\\.sql"))
// sort by number-prefix
.sortBy(file => {
file.getName.split("\\.")(0).split("_")(0).toInt
})
.toSeq
sqlFiles.zip(1 to sqlFiles.size)
.map {
case (file, revision) => {
val script = readStreamAsString(file.toURI.toURL.openStream())
val parsed = Collections.unfoldLeft(("", script.split('\n').toList.map(_.trim))) {
case (_, Nil) => None
case (context, lines) => {
val (some, next) = lines.span(l => !isMarker(l))
Some((next.headOption.map(c => (mapUpsAndDowns(c), next.tail)).getOrElse("" -> Nil),
context -> some.mkString("\n")))
}
}.reverse.drop(1).groupBy(i => i._1).mapValues { _.map(_._2).mkString("\n").trim }
Evolution(
revision,
parsed.getOrElse(UPS, ""),
parsed.getOrElse(DOWNS, "")
)
}
}
}
/**
* Read the given stream into a byte array.
*
* Closes the stream.
*/
private def readStream(stream: InputStream): Array[Byte] = {
try {
val buffer = new Array[Byte](8192)
var len = stream.read(buffer)
val out = new ByteArrayOutputStream() // Doesn't need closing
while (len != -1) {
out.write(buffer, 0, len)
len = stream.read(buffer)
}
out.toByteArray
} finally closeQuietly(stream)
}
/**
* Read the given stream into a String.
*
* Closes the stream.
*/
def readStreamAsString(stream: InputStream)(implicit codec: Codec): String = {
new String(readStream(stream), codec.name)
}
/**
* Close the given closeable quietly.
*
* Logs any IOExceptions encountered.
*/
def closeQuietly(closeable: Closeable) = {
try {
if (closeable != null) {
closeable.close()
}
} catch {
case e: IOException => Logger.warn("Error closing stream", e)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment