Skip to content

Commit

Permalink
PostLoad event listener
Browse files Browse the repository at this point in the history
  • Loading branch information
gonmarques committed Feb 18, 2017
1 parent 1252808 commit f4621c8
Show file tree
Hide file tree
Showing 20 changed files with 487 additions and 21 deletions.
2 changes: 1 addition & 1 deletion project/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ object Build extends Build {

name := "slick-repo",
description := "CRUD Repositories for Slick based persistence Scala projects",
version := "1.2.8-SNAPSHOT",
version := "1.3.1-SNAPSHOT",

scalaVersion := "2.11.8",
crossScalaVersions := Seq("2.11.8", "2.12.1", "2.10.6"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* MIT License
*
* Copyright (c) 2017 Gonçalo Marques
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package com.byteslounge.slickrepo.repository

import java.util.concurrent.ConcurrentHashMap

import scala.collection.concurrent.Map
import scala.collection.convert.decorateAsScala._

class LifecycleHandler {

private[repository] val lifecycleHandlerCache: Map[LifecycleHandlerCacheKey, Boolean] = new ConcurrentHashMap[LifecycleHandlerCacheKey, Boolean]().asScala

def isLifecycleHandlerDefined(clazz: Class[_ <: Repository[_, _]], event: LifecycleEvent): Boolean = {
val key = LifecycleHandlerCacheKey(clazz, event)
val entry = lifecycleHandlerCache.get(key)
if(entry.isDefined) {
entry.get
} else {
val overridden = isHandlerOverridden(clazz, event)
lifecycleHandlerCache.put(key, overridden)
overridden
}
}

private def isHandlerOverridden(clazz: Class[_ <: Repository[_, _]], event: LifecycleEvent): Boolean = {
clazz.getMethod(event.functionName).getDeclaringClass != classOf[Repository[_, _]]
}

}

object LifecycleHandler extends LifecycleHandler

private case class LifecycleHandlerCacheKey(clazz: Class[_ <: Repository[_, _]], event: LifecycleEvent)

sealed abstract class LifecycleEvent(val functionName : String)
case object POSTLOAD extends LifecycleEvent("postLoad")
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@ abstract class Repository[T <: Entity[T, ID], ID](val driver: JdbcProfile) {
/**
* Finds all entities.
*/
def findAll(): DBIO[Seq[T]] = {
tableQueryCompiled.result
def findAll()(implicit ec: ExecutionContext): DBIO[Seq[T]] = {
tableQueryCompiled.result.map(seq => sequenceLifecycleEvent(seq, postLoad, POSTLOAD))
}

/**
* Finds a given entity by its primary key.
*/
def findOne(id: ID): DBIO[Option[T]] = {
findOneCompiled(id).result.headOption
def findOne(id: ID)(implicit ec: ExecutionContext): DBIO[Option[T]] = {
findOneCompiled(id).result.headOption.map(e => e.map(postLoad))
}

/**
Expand All @@ -83,16 +83,10 @@ abstract class Repository[T <: Entity[T, ID], ID](val driver: JdbcProfile) {
*/
def save(entity: T)(implicit ec: ExecutionContext): DBIO[T] =
entity.id match {
case None => saveUsingGeneratedId(entity)
case Some(_) => saveUsingPredefinedId(entity)
case None => generatedIdPersister(entity, ec)
case Some(_) => predefinedIdPersister(entity, ec)
}

/**
* Persists an entity using an auto-generated primary key.
*/
private def saveUsingGeneratedId(entity: T)(implicit ec: ExecutionContext): DBIO[T] =
generatedIdPersister(entity, ec)

/**
* Generated ID persister
*/
Expand All @@ -108,12 +102,6 @@ abstract class Repository[T <: Entity[T, ID], ID](val driver: JdbcProfile) {
(saveCompiled += transformed).map(id => transformed.withId(id))(ec)
}

/**
* Persists an entity using a predefined primary key.
*/
protected def saveUsingPredefinedId(entity: T)(implicit ec: ExecutionContext): DBIO[T] =
predefinedIdPersister(entity, ec)

/**
* Predefined ID persister
*/
Expand Down Expand Up @@ -226,9 +214,18 @@ abstract class Repository[T <: Entity[T, ID], ID](val driver: JdbcProfile) {
}
}

private def sequenceLifecycleEvent(seq: Seq[T], handler: T => T, event: LifecycleEvent): Seq[T] = {
if (LifecycleHandler.isLifecycleHandlerDefined(this.getClass, event)) {
seq.map(handler)
} else {
seq
}
}

lazy protected val tableQueryCompiled = Compiled(tableQuery)
lazy protected val findOneCompiled = Compiled((id: Rep[ID]) => tableQuery.filter(_.id === id))
lazy protected val saveCompiled = tableQuery returning tableQuery.map(_.id)
lazy private val countCompiled = Compiled(tableQuery.map(_.id).length)

val postLoad: (T => T) = (entity) => entity
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* MIT License
*
* Copyright (c) 2017 Gonçalo Marques
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package com.byteslounge.slickrepo.repository

import com.byteslounge.slickrepo.test.LifecycleEntityRepository
import org.scalatest.{BeforeAndAfter, FlatSpec, Matchers}

class LifecycleHandlerTest extends FlatSpec with BeforeAndAfter with Matchers {

"The LifecycleHandler" should "detect that an entity does not define a handler" in {
LifecycleHandler.isLifecycleHandlerDefined(classOf[PersonRepository], POSTLOAD) should equal(false)
}

it should "detect that an entity defines a handler" in {
LifecycleHandler.isLifecycleHandlerDefined(classOf[LifecycleEntityRepository], POSTLOAD) should equal(true)
}

it should "cache the result for an entity that does not define a handler" in {
val key = LifecycleHandlerCacheKey(classOf[PersonRepository], POSTLOAD)
LifecycleHandler.lifecycleHandlerCache.clear()
LifecycleHandler.lifecycleHandlerCache.get(key).isDefined should equal(false)
LifecycleHandler.isLifecycleHandlerDefined(classOf[PersonRepository], POSTLOAD)
LifecycleHandler.lifecycleHandlerCache.get(key).get should equal(false)
}

it should "cache the result for an entity that defines a handler" in {
val key = LifecycleHandlerCacheKey(classOf[LifecycleEntityRepository], POSTLOAD)
LifecycleHandler.lifecycleHandlerCache.clear()
LifecycleHandler.lifecycleHandlerCache.get(key).isDefined should equal(false)
LifecycleHandler.isLifecycleHandlerDefined(classOf[LifecycleEntityRepository], POSTLOAD)
LifecycleHandler.lifecycleHandlerCache.get(key).get should equal(true)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ abstract class AbstractRepositoryTest(val config: Config) extends FlatSpec with
val testLongVersionedEntityRepository = new TestLongVersionedEntityRepository(driver)
val testInstantVersionedEntityRepository = new TestInstantVersionedEntityRepository(driver)
val testJodaTimeVersionedEntityRepository = new TestJodaTimeVersionedEntityRepository(driver)
val lifecycleEntityRepository = new LifecycleEntityRepository(driver)
val lifecycleVersionedEntityRepository = new LifecycleVersionedEntityRepository(driver)

def executeAction[X](action: DBIOAction[X, NoStream, _]): X = {
Await.result(db.run(action), Duration.Inf)
Expand Down Expand Up @@ -84,7 +86,9 @@ abstract class AbstractRepositoryTest(val config: Config) extends FlatSpec with
testIntegerVersionedAutoPkEntityRepository.tableQuery.schema.create,
testLongVersionedEntityRepository.tableQuery.schema.create,
testInstantVersionedEntityRepository.tableQuery.schema.create,
testJodaTimeVersionedEntityRepository.tableQuery.schema.create
testJodaTimeVersionedEntityRepository.tableQuery.schema.create,
lifecycleEntityRepository.tableQuery.schema.create,
lifecycleVersionedEntityRepository.tableQuery.schema.create
)
)
}
Expand All @@ -99,7 +103,9 @@ abstract class AbstractRepositoryTest(val config: Config) extends FlatSpec with
testIntegerVersionedAutoPkEntityRepository.tableQuery.schema.drop,
testLongVersionedEntityRepository.tableQuery.schema.drop,
testInstantVersionedEntityRepository.tableQuery.schema.drop,
testJodaTimeVersionedEntityRepository.tableQuery.schema.drop
testJodaTimeVersionedEntityRepository.tableQuery.schema.drop,
lifecycleEntityRepository.tableQuery.schema.drop,
lifecycleVersionedEntityRepository.tableQuery.schema.drop
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ abstract class InstantVersionedRepositoryTest(override val config: Config) exten
}

it should "perform a batch insert of instant versioned entities" in {
import scala.concurrent.ExecutionContext.Implicits.global
val batchInsertAction = testInstantVersionedEntityRepository.batchInsert(
Seq(TestInstantVersionedEntity(Option(1), 2.2, None), TestInstantVersionedEntity(Option(2), 3.3, None), TestInstantVersionedEntity(Option(3), 4.4, None))
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ abstract class IntegerVersionedRepositoryAutoPkTest(override val config: Config)
}

it should "perform a batch insert of auto pk integer versioned entities" in {
import scala.concurrent.ExecutionContext.Implicits.global
val batchInsertAction = testIntegerVersionedAutoPkEntityRepository.batchInsert(
Seq(TestIntegerVersionedAutoPkEntity(None, 2.2, None), TestIntegerVersionedAutoPkEntity(None, 3.3, None), TestIntegerVersionedAutoPkEntity(None, 4.4, None))
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ abstract class IntegerVersionedRepositoryTest(override val config: Config) exten
}

it should "perform a batch insert of manual pk integer versioned entities" in {
import scala.concurrent.ExecutionContext.Implicits.global
val batchInsertAction = testIntegerVersionedEntityRepository.batchInsert(
Seq(TestIntegerVersionedEntity(Option(1), 2.2, None), TestIntegerVersionedEntity(Option(2), 3.3, None), TestIntegerVersionedEntity(Option(3), 4.4, None))
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ abstract class JodaTimeVersionedRepositoryTest(override val config: Config) exte
}

it should "perform a batch insert of joda time versioned entities" in {
import scala.concurrent.ExecutionContext.Implicits.global
val batchInsertAction = testJodaTimeVersionedEntityRepository.batchInsert(
Seq(TestJodaTimeVersionedEntity(Option(1), 2.2, None), TestJodaTimeVersionedEntity(Option(2), 3.3, None), TestJodaTimeVersionedEntity(Option(3), 4.4, None))
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ abstract class LongVersionedRepositoryTest(override val config: Config) extends
}

it should "perform a batch insert of long versioned entities" in {
import scala.concurrent.ExecutionContext.Implicits.global
val batchInsertAction = testLongVersionedEntityRepository.batchInsert(
Seq(TestLongVersionedEntity(Option(1), 2.2, None), TestLongVersionedEntity(Option(2), 3.3, None), TestLongVersionedEntity(Option(3), 4.4, None))
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* MIT License
*
* Copyright (c) 2016 Gonçalo Marques
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package com.byteslounge.slickrepo.test

import com.byteslounge.slickrepo.meta.{Entity, Keyed, Versioned, VersionedEntity}
import com.byteslounge.slickrepo.repository._
import com.byteslounge.slickrepo.scalaversion.JdbcProfile
import slick.ast.BaseTypedType

abstract class RepositoryLifecycleEventsTest(override val config: Config) extends AbstractRepositoryTest(config) {

"The Repository Lifecycle events" should "trigger a postLoad event - non versioned entity - findOne" in {
import scala.concurrent.ExecutionContext.Implicits.global
val entity: LifecycleEntity = executeAction(lifecycleEntityRepository.save(LifecycleEntity(None, "john")))
val read: LifecycleEntity = executeAction(lifecycleEntityRepository.findOne(entity.id.get)).get
read.name should equal("postLoad")
}

it should "trigger a postLoad event - versioned entity - findOne" in {
import scala.concurrent.ExecutionContext.Implicits.global
val entity: LifecycleVersionedEntity = executeAction(lifecycleVersionedEntityRepository.save(LifecycleVersionedEntity(None, "john", None)))
val read: LifecycleVersionedEntity = executeAction(lifecycleVersionedEntityRepository.findOne(entity.id.get)).get
read.name should equal("postLoad")
}

it should "trigger a postLoad event - non versioned entity - findAll" in {
import scala.concurrent.ExecutionContext.Implicits.global
executeAction(lifecycleEntityRepository.save(LifecycleEntity(None, "john")))
val read: Seq[LifecycleEntity] = executeAction(lifecycleEntityRepository.findAll())
read.head.name should equal("postLoad")
}

it should "trigger a postLoad event - versioned entity - findAll" in {
import scala.concurrent.ExecutionContext.Implicits.global
executeAction(lifecycleVersionedEntityRepository.save(LifecycleVersionedEntity(None, "john", None)))
val read: Seq[LifecycleVersionedEntity] = executeAction(lifecycleVersionedEntityRepository.findAll())
read.head.name should equal("postLoad")
}
}

case class LifecycleEntity(override val id: Option[Int] = None, name: String) extends Entity[LifecycleEntity, Int]{
def withId(id: Int): LifecycleEntity = this.copy(id = Some(id))
}

class LifecycleEntityRepository(override val driver: JdbcProfile) extends Repository[LifecycleEntity, Int](driver) {

import driver.api._
val pkType = implicitly[BaseTypedType[Int]]
val tableQuery = TableQuery[LifecycleEntities]
type TableType = LifecycleEntities

class LifecycleEntities(tag: slick.lifted.Tag) extends Table[LifecycleEntity](tag, "LIFECYC_ENT") with Keyed[Int] {
def id = column[Int]("ID", O.PrimaryKey, O.AutoInc)
def name = column[String]("NAME")

def * = (id.?, name) <> ((LifecycleEntity.apply _).tupled, LifecycleEntity.unapply)
}

override val postLoad = (e: LifecycleEntity) => e.copy(name = "postLoad")

}

case class LifecycleVersionedEntity(override val id: Option[Int] = None, name: String, override val version: Option[Int]) extends VersionedEntity[LifecycleVersionedEntity, Int, Int]{
def withId(id: Int): LifecycleVersionedEntity = this.copy(id = Some(id))
def withVersion(version: Int): LifecycleVersionedEntity = this.copy(version = Some(version))
}

class LifecycleVersionedEntityRepository(override val driver: JdbcProfile) extends VersionedRepository[LifecycleVersionedEntity, Int, Int](driver) {

import driver.api._
val pkType = implicitly[BaseTypedType[Int]]
val versionType = implicitly[BaseTypedType[Int]]
val tableQuery = TableQuery[LifecycleVersionedEntities]
type TableType = LifecycleVersionedEntities

class LifecycleVersionedEntities(tag: slick.lifted.Tag) extends Table[LifecycleVersionedEntity](tag, "LIFECYC_V_ENT") with Versioned[Int, Int] {
def id = column[Int]("ID", O.PrimaryKey, O.AutoInc)
def name = column[String]("NAME")
def version = column[Int]("VERSION")

def * = (id.?, name, version.?) <> ((LifecycleVersionedEntity.apply _).tupled, LifecycleVersionedEntity.unapply)
}

override val postLoad = (e: LifecycleVersionedEntity) => e.copy(name = "postLoad")

}

0 comments on commit f4621c8

Please sign in to comment.