app/models/PostrunAction.scala (166 lines of code) (raw):
package models
import java.io.File
import java.nio.file.{Files, Path, Paths}
import java.sql.Timestamp
import helpers.{JythonOutput, PostrunDataCache}
import org.apache.commons.io.{FileUtils, FilenameUtils}
import org.slf4j.LoggerFactory
import play.api.{Configuration, Logger}
import play.api.libs.json.{JsPath, Reads, Writes}
import play.api.libs.functional.syntax._
import postrun.PojoPostrun
import slick.lifted.{TableQuery, Tag}
import slick.jdbc.PostgresProfile.api._
import scala.concurrent.Future
import scala.util.{Failure, Success, Try}
import scala.concurrent.ExecutionContext.Implicits.global
case class PostrunAction (id:Option[Int],runnable:String, title:String, description:Option[String],
owner:String, version:Int, ctime: Timestamp) extends PlutoModel {
private val logger = LoggerFactory.getLogger(getClass)
/**
* writes this model into the database, inserting if id is None and returning a fresh object with id set. If an id
* was set, then returns the same object. */
def save(implicit db: slick.jdbc.PostgresProfile#Backend#Database):Future[Try[PostrunAction]] = id match {
case None=>
val insertQuery = TableQuery[PostrunActionRow] returning TableQuery[PostrunActionRow].map(_.id) into ((item,id)=>item.copy(id=Some(id)))
db.run(
(insertQuery+=this).asTry
).map({
case Success(insertResult)=>Success(insertResult.asInstanceOf[PostrunAction]) //maybe only intellij needs the cast here?
case Failure(error)=>Failure(error)
})
case Some(realEntityId)=>
db.run(
TableQuery[PostrunActionRow].filter(_.id===realEntityId).update(this).asTry
).map({
case Success(rowsAffected)=>Success(this)
case Failure(error)=>Failure(error)
})
}
/**
* asynchronously creates a backup of the given project file as a temp file
* @param projectFileName project file to back up
* @return a Future, containing a Try that contains either the backup file created or an error indicating why it was not created
*/
def backupProjectFile(projectFileName: String): Future[Try[Path]] = Future {
Try {
val projectNameOnly = FilenameUtils.getBaseName(projectFileName)
val outputPath = Files.createTempFile(projectNameOnly, ".bak")
FileUtils.copyFile(new File(projectFileName), new File(outputPath.toString))
outputPath
}
}
/**
* synchronously copies the given backup file back over the original file
* @param backupPath Path representing the backup file
* @param originalFile String representing the file path to copy it over
* @return a Try indicating whether the operation succeeded or not
*/
def restoreBackupFile(backupPath: Path, originalFile: String) = Try {
val logger = Logger(this.getClass)
logger.warn(s"Restoring backup file ${backupPath.toString}")
FileUtils.copyFile(new File(backupPath.toString), new File(originalFile))
}
/**
* returns the on-disk path for the executable script
*/
def getScriptPath(implicit config:Configuration) = Paths.get(config.get[String]("postrun.scriptsPath"), this.runnable)
protected def initialisePojoClass(className:String, config:Configuration) = {
logger.debug(s"Trying to initialise pojo $className using configuration-enabled constructor....")
Try { Class.forName(className).getDeclaredConstructor(classOf[Configuration]) } match {
case Success(constructor)=>
Try { constructor.newInstance(config).asInstanceOf[PojoPostrun] }
case Failure(err)=>
logger.debug(s"Could not initialise pojo ${className} with config: $err, trying with non-configuration constructor...")
Try { Class.forName(className).getDeclaredConstructor() } match {
case Success(constructor)=>
Try { constructor.newInstance().asInstanceOf[PojoPostrun] }
case Failure(err)=>
logger.error(s"Could not initialise $className:", err)
Failure(new RuntimeException(s"Could not initialise $className: ${err.getMessage}"))
}
}
}
/**
* Runs the provided java class as a postrun
* @param projectFileName
* @param projectEntry
* @param projectType
* @param dataCache
* @param workingGroupMaybe
* @param commissionMaybe
* @param config
* @return
*/
protected def runPojo(projectFileName:String,projectEntry:ProjectEntry,projectType:ProjectType,dataCache:PostrunDataCache,
workingGroupMaybe: Option[PlutoWorkingGroup], commissionMaybe: Option[PlutoCommission])
(implicit config:Configuration):Future[Try[JythonOutput]] = {
val className: String = runnable.substring(5) //strip off "java:" prefix
val logger = Logger(this.getClass)
logger.debug(s"Initiating java based postrun $className...")
initialisePojoClass(className, config) match {
case Success(postrunClass) =>
logger.debug(s"Successfully initialised $className")
postrunClass.postrun(projectFileName, projectEntry, projectType, dataCache, workingGroupMaybe, commissionMaybe).map({
case Success(newDataCache) =>
logger.debug(s"Postrun executed successfully")
Success(JythonOutput("", "", newDataCache, raisedError = None))
case Failure(error) =>
logger.debug(s"Postrun indicated error")
Success(JythonOutput("", "", dataCache, raisedError = Some(error)))
}).recoverWith({
case ex: Throwable => Future(Failure(ex))
})
case Failure(err) =>
Future(Failure(err))
}
}
/**
* returns true of this postrun is a java object (Plain Old Java Object => POJO) or False if it's python
*/
def isPojo:Boolean = runnable.startsWith("java:")
/**
* asynchronously executes this postrun action on a newly created project
* @param projectFileName - filename of the newly created project
* @param projectEntry - models.projectEntry object representing the newly created project
* @param projectType - models.projectType that the project was created from
* @param config - implicitly provided play.api.Configuration object, representing the app configuration
* @return a Future containing a Try containing either the script output or an error
*/
def run(projectFileName:String,projectEntry:ProjectEntry,projectType:ProjectType,dataCache:PostrunDataCache,
workingGroupMaybe: Option[PlutoWorkingGroup], commissionMaybe: Option[PlutoCommission])
(implicit config:Configuration):Future[Try[JythonOutput]] = {
backupProjectFile(projectFileName) flatMap {
case Failure(error) =>
val logger = Logger(this.getClass)
logger.error(s"Unable to back up project file $projectFileName:", error)
Future(Failure(error))
case Success(backupPath) =>
val logger = Logger(this.getClass)
logger.info(s"Backed up project file from $projectFileName to ${backupPath.toString}")
logger.debug(s"Going to try to run script at path $getScriptPath...")
val resultFuture = if(isPojo){
runPojo(projectFileName, projectEntry, projectType, dataCache, workingGroupMaybe, commissionMaybe)
} else {
Future(Failure(new RuntimeException("Unsupported postrun type")))
}
resultFuture.map({
case Failure(error) =>
logger.error(s"Unable to start postrun script: ${error.getMessage}", error)
restoreBackupFile(backupPath, projectFileName) match {
case Failure(restoreError)=>
logger.error(s"Cannot restore backup, project file $projectFileName may be corrupted")
Failure(error)
case Success(unitval)=>
Failure(error)
}
case Success(result)=>Success(result)
})
}
}
}
object PostrunAction extends ((Option[Int],String,String,Option[String],String,Int,Timestamp)=>PostrunAction) {
def entryForRunnable(scriptName:String)(implicit db:slick.jdbc.PostgresProfile#Backend#Database):Future[Try[Seq[PostrunAction]]] =
db.run(
TableQuery[PostrunActionRow].filter(_.runnable===scriptName).result.asTry
)
def allEntries(implicit db:slick.jdbc.PostgresProfile#Backend#Database):Future[Try[Seq[PostrunAction]]] =
db.run(
TableQuery[PostrunActionRow].result.asTry
)
def allPython(implicit db:slick.jdbc.PostgresProfile#Backend#Database):Future[Try[Seq[PostrunAction]]] =
db.run(
TableQuery[PostrunActionRow].filter(_.runnable like "%.py").result.asTry
)
}
class PostrunActionRow(tag:Tag) extends Table[PostrunAction](tag, "PostrunAction") {
def id = column[Int]("id",O.PrimaryKey, O.AutoInc)
def runnable = column[String]("s_runnable")
def title = column[String]("s_title")
def description = column[Option[String]]("s_description")
def owner = column[String]("s_owner")
def version = column[Int]("i_version")
def ctime = column[Timestamp]("t_ctime")
def * = (id.?, runnable, title, description, owner, version, ctime) <> (PostrunAction.tupled, PostrunAction.unapply)
}
trait PostrunActionSerializer extends TimestampSerialization {
implicit val postrunActionWrites:Writes[PostrunAction] = (
(JsPath \ "id").writeNullable[Int] and
(JsPath \ "runnable").write[String] and
(JsPath \ "title").write[String] and
(JsPath \ "description").writeNullable[String] and
(JsPath \ "owner").write[String] and
(JsPath \ "version").write[Int] and
(JsPath \ "ctime").write[Timestamp]
)(unlift(PostrunAction.unapply))
implicit val postrunActionReads:Reads[PostrunAction] = (
(JsPath \ "id").readNullable[Int] and
(JsPath \ "runnable").read[String] and
(JsPath \ "title").read[String] and
(JsPath \ "description").readNullable[String] and
(JsPath \ "owner").read[String] and
(JsPath \ "version").read[Int] and
(JsPath \ "ctime").read[Timestamp]
)(PostrunAction.apply _)
}