All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.tuplejump.plugin.CassandraPlugin.scala Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to Tuplejump Software Pvt. Ltd. under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. Tuplejump Software Pvt. Ltd. licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.tuplejump.plugin

import java.util.Date

import com.datastax.driver.core.exceptions.NoHostAvailableException
import com.datastax.driver.core.{Cluster, Session, ResultSet}
import play.api.Play.current
import play.api.{Application, Play, Plugin}

import scala.io.Source
import scala.util.{Failure, Success, Try}

class CassandraPlugin(app: Application) extends Plugin {

  private var _helper: Option[CassandraConnection] = None

  def helper = _helper.getOrElse(throw new RuntimeException("CassandraPlugin error: CassandraHelper initialization failed"))

  override def onStart() = {

    val appConfig = app.configuration.getConfig("cassandraPlugin").get
    val appName: String = appConfig.getString("appName").getOrElse("appWithCassandraPlugin")

    val isEvolutionEnabled: Boolean = appConfig.getBoolean("evolution.enabled").getOrElse(true)
    val scriptSource: String = appConfig.getString("evolution.directory").getOrElse("evolutions/cassandra/")

    val hosts: Array[java.lang.String] = appConfig.getString("host").getOrElse("localhost").split(",").map(_.trim)
    val port: Int = appConfig.getInt("port").getOrElse(9042)

    val cluster = Cluster.builder()
      .addContactPoints(hosts: _*)
      .withPort(port).build()

    _helper = try {
      val session = cluster.connect()
      Util.loadScript("cassandraPlugin.cql", session)
      if (isEvolutionEnabled) {
        Evolutions.applyEvolution(session, appName, scriptSource)
      }
      Some(CassandraConnection(hosts, port, cluster, session))
    } catch {
      case e: NoHostAvailableException =>
        val msg =
          s"""Failed to initialize CassandraPlugin.
             |Please check if Cassandra is accessible at
             | ${hosts.head}:$port or update configuration""".stripMargin
        throw app.configuration.globalError(msg)
    }
  }

  override def onStop() = {
    helper.session.close()
    helper.cluster.close()
  }

  override def enabled = true

}

private[plugin] object Util {
  private def isComment(statement: String): Boolean = {
    statement.startsWith("#")
  }

  private def isValidStatement(str: String): Boolean = {
    val line = str.trim
    line.length == 0 || isComment(line)
  }

  def getValidStatements(lines: Iterator[String]): Array[String] = {
    lines.filterNot(isValidStatement).mkString("").split(";")
  }

  private def getValidStatementsFromFile(fileName: String): Array[String] = {
    val lines = Source.fromURL(getClass.getClassLoader.getResource(fileName)).getLines()
    getValidStatements(lines)
  }

  def executeStmnts(stmnts: Array[String], session: Session) = {
    stmnts.foreach {
      line =>
        try {
          session.execute(line)
        } catch {
          case ex: Throwable =>
            throw new RuntimeException(s"Failed to execute $line", ex)
        }
    }
  }

  def loadScript(fileName: String, session: Session): Unit = {
    val stmnts = getValidStatementsFromFile(fileName)
    executeStmnts(stmnts, session)
  }

}

private[plugin] object Evolutions {

  import com.datastax.driver.core.querybuilder.{QueryBuilder => QB}

  import scala.collection.JavaConversions._

  private val Keyspace = "cassandra_play_plugin"
  private val Table = "revision_history"
  private val AppIDColumn = "app_id"
  private val RevisionColumn = "revision"
  private val TimeColumn = "applied_at"

  private def getLastUpdateRevision(session: Session, appName: String): Int = {
    val query = QB.select(RevisionColumn)
      .from(Keyspace, Table)
      .where(QB.eq(AppIDColumn, appName))

    val row = session.execute(query).toIterable.headOption
    val result = row.map(_.getInt(RevisionColumn))

    result.getOrElse(0)
  }

  private def updateRevision(session: Session,
                             appName: String,
                             revision: Int): ResultSet = {
    val query = QB.update(Keyspace, Table)
      .`with`(QB.set(RevisionColumn, revision))
      .and(QB.set(TimeColumn, new Date()))
      .where(QB.eq(AppIDColumn, appName))

    session.execute(query)
  }

  private def updateDBFromRevision(session: Session,
                                   appName: String,
                                   dirName: String,
                                   revision: Int): Boolean = {
    val currentRevision = revision + 1
    val fileName: String = s"$dirName$currentRevision.cql"

    val mayBeLines = Try(Source.fromURL(getClass.getClassLoader.getResource(fileName)).getLines())

    mayBeLines match {
      case Success(lines) =>
        val stmt = Util.getValidStatements(lines)
        Util.executeStmnts(stmt, session)
        updateRevision(session, appName, currentRevision)
        updateDBFromRevision(session, appName, dirName, currentRevision)
      case Failure(e: NullPointerException) =>
        //no more cql files to run
        false
      case Failure(e) =>
        throw e
    }
  }

  def applyEvolution(session: Session, appName: String, dirName: String) = {
    val lastUpdatedRevision = getLastUpdateRevision(session, appName)

    updateDBFromRevision(session, appName, dirName, lastUpdatedRevision)
  }
}

object Cassandra {
  private val casPlugin = Play.application.plugin[CassandraPlugin].get

  private val cassandraHelper = casPlugin.helper

  /**
   * gets the Cassandra hosts provided in the configuration
   */
  def hosts: Array[java.lang.String] = cassandraHelper.hosts

  /**
   * gets the port number on which Cassandra is running from the configuration
   */
  def port: Int = cassandraHelper.port

  /**
   * gets a reference of the started Cassandra cluster
   * The cluster is built with the configured set of initial contact points
   * and policies at startup
   */
  def cluster: Cluster = cassandraHelper.cluster

  /**
   * gets a reference of the started Cassandra session
   * A new session is created on the cluster at startup
   */
  def session: Session = cassandraHelper.session

  /**
   * executes CQL statements available in given file.
   * Empty lines or lines starting with `#` are ignored.
   * Each statement can extend over multiple lines and must end with a semi-colon.
   * @param fileName - name of the file
   */
  def loadCQLFile(fileName: String): Unit = {
    Util.loadScript(fileName, cassandraHelper.session)
  }

}

private[plugin] case class CassandraConnection(hosts: Array[java.lang.String],
                                               port: Int,
                                               cluster: Cluster,
                                               session: Session)




© 2015 - 2024 Weber Informatics LLC | Privacy Policy