PageRenderTime 118ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/boxes/src/boxes/persistence/mongo/MongoBox.scala

http://github.com/trepidacious/boxes
Scala | 185 lines | 138 code | 33 blank | 14 comment | 5 complexity | e96ed59ec6befcc0fdb6521166443503 MD5 | raw file
  1. package boxes.persistence.mongo
  2. import com.mongodb.casbah.Imports._
  3. import boxes.persistence.json.JSONIO
  4. import scala.collection.mutable.WeakHashMap
  5. import boxes.demo.Person
  6. import com.mongodb.DBObject
  7. import com.mongodb.util.JSON
  8. import boxes._
  9. import boxes.persistence.ClassAliases
  10. import boxes.persistence.CodecByClass
  11. import boxes.util.WeakKeysBIDIMap
  12. import com.mongodb.casbah.commons.MongoDBObject
  13. import com.mongodb.MongoException
  14. object MongoBox {
  15. def main(args: Array[String]) {
  16. val aliases = {
  17. val a = new ClassAliases
  18. a.alias(classOf[TestNode], "TestNode")
  19. a
  20. }
  21. val mb = new MongoBox("boxestest", aliases)
  22. val bob = new TestNode
  23. bob.index() = 1
  24. val bobDup = new TestNode
  25. bobDup.index() = 2
  26. val bill = new TestNode
  27. bill.name() = "bill"
  28. bill.index() = 42
  29. mb.keep(bobDup)
  30. mb.keep(bob)
  31. mb.keep(bill)
  32. }
  33. def tryOption[T](f: =>T): Option[T] = {
  34. try {
  35. Some(f)
  36. } catch {
  37. case c => None
  38. }
  39. }
  40. }
  41. import MongoBox._
  42. object TestNode extends MongoMetaNode {
  43. override val indices = List(MongoNodeIndex("name"))
  44. }
  45. class TestNode extends MongoNode {
  46. def meta = TestNode
  47. val name = Var("bob")
  48. val index = Var(0)
  49. }
  50. case class MongoNodeIndex(key: String, unique: Boolean = true, ascending: Boolean = true)
  51. trait MongoMetaNode {
  52. def indices: List[MongoNodeIndex] = List()
  53. }
  54. trait MongoNode extends Node {
  55. def meta: MongoMetaNode
  56. }
  57. class MongoBox(dbName: String, aliases: ClassAliases) {
  58. private val mongoConn = MongoConnection()
  59. private val db = mongoConn(dbName)
  60. private val io = JSONIO(aliases)
  61. //TODO might be better as soft references, to reduce unneeded db access?
  62. private val m = new WeakKeysBIDIMap[Node, ObjectId]()
  63. def id(t: Node) = Box.transact{m.toValue(t)}
  64. private def toNode[T <: Node](alias: String, dbo: MongoDBObject) = {
  65. for {
  66. id <- dbo._id
  67. } yield {
  68. m.toKey(id).map(_.asInstanceOf[T]).getOrElse {
  69. val fromMongo = io.readDBO(dbo).asInstanceOf[T]
  70. track(alias, fromMongo, id)
  71. fromMongo
  72. }
  73. }
  74. }
  75. def findById[T <: Node](id: String)(implicit man: Manifest[T]): Option[T] =
  76. tryOption(new ObjectId(id)).flatMap(oid => findById(oid)(man))
  77. def findById[T <: Node](id: ObjectId)(implicit man: Manifest[T]): Option[T] = findOne(MongoDBObject("_id" -> id))(man)
  78. def findOne[T <: Node](key: String, value: Any)(implicit man: Manifest[T]): Option[T] = findOne(MongoDBObject(key -> value))(man)
  79. def findOne[T <: Node](query: MongoDBObject)(implicit man: Manifest[T]): Option[T] = {
  80. Box.transact {
  81. val alias = aliases.forClass(man.erasure)
  82. for {
  83. dbo <- db(alias).findOne(query)
  84. n <- toNode[T](alias, dbo)
  85. } yield n
  86. }
  87. }
  88. def find[T <: Node](query: MongoDBObject)(implicit man: Manifest[T]): Iterator[T] = {
  89. Box.transact {
  90. val alias = aliases.forClass(man.erasure)
  91. val cursor = db(alias).find(query)
  92. cursor.flatMap(dbo => toNode[T](alias, dbo))
  93. }
  94. }
  95. private def useMongoNode(alias: String, t: Node) {
  96. t match {
  97. case mn: MongoNode => mn.meta.indices.foreach(
  98. i => db(alias).ensureIndex(
  99. MongoDBObject(i.key -> (if (i.ascending) "1" else "-1")),
  100. i.key,
  101. i.unique))
  102. case _ => {}
  103. }
  104. }
  105. private def track(alias:String, t: Node, id: ObjectId) {
  106. //First make sure any indices are in place, so we respect them from the View we will create
  107. useMongoNode(alias, t)
  108. //Set up a View that writes any changes to mongo
  109. val query = MongoDBObject("_id" -> id)
  110. t.retainReaction(View {
  111. val dbo = io.writeDBO(t).asInstanceOf[MongoDBObject]
  112. db(alias).update(query, dbo)
  113. })
  114. m.put(t, id)
  115. }
  116. //Register a Node to be kept in mongodb. Returns the ObjectId used. If the
  117. //Node was already kept, nothing is done, but the ObjectId is still returned.
  118. def keep(t: Node) = {
  119. Box.transact{
  120. //Get the existing id for the node, or else add the
  121. //node to mongo and return the new id
  122. m.toValue(t).getOrElse{
  123. val alias = aliases.forClass(t.getClass())
  124. //First make sure any indices are in place, so we respect them when writing the
  125. //new record to DB
  126. useMongoNode(alias, t)
  127. //Make a DB object with new id, and insert it to mongo
  128. val id = new ObjectId()
  129. val dbo = io.writeDBO(t).asInstanceOf[MongoDBObject]
  130. dbo.put("_id", id)
  131. //TODO detect errors, e.g. index problems (duplicate ObjectId, or conflict with an index from MongoNode)
  132. db(alias).insert(dbo)
  133. val leOrNull = db(alias).lastError.getException()
  134. if (leOrNull != null) throw leOrNull
  135. //Set up View and add to map
  136. track(alias, t, id)
  137. id
  138. }
  139. }
  140. }
  141. def forget(t: Node) {
  142. Box.transact{
  143. //Get the existing id for the node
  144. m.toValue(t).foreach(id => {
  145. val alias = aliases.forClass(t.getClass())
  146. //Remove from mongo and our map
  147. db(alias).remove(MongoDBObject("_id" -> id))
  148. m.removeKey(t)
  149. })
  150. }
  151. }
  152. }