From 2f72d8cf7efadbafb1656ad6bb02fd896cdd1a46 Mon Sep 17 00:00:00 2001 From: Tim Nelson Date: Thu, 10 Mar 2011 15:02:12 -0600 Subject: [PATCH] Add better support for referenced records in mongodb --- .../mongodb/record/field/MongoListField.scala | 3 +- .../mongodb/record/field/MongoRefField.scala | 135 ++++++++++++++++++ .../net/liftweb/mongodb/record/Fixtures.scala | 60 +++++--- .../mongodb/record/MongoRecordSpec.scala | 26 ++++ 4 files changed, 206 insertions(+), 18 deletions(-) create mode 100644 persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/MongoRefField.scala diff --git a/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/MongoListField.scala b/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/MongoListField.scala index 5faaf84a94..3427f58ed5 100644 --- a/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/MongoListField.scala +++ b/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/MongoListField.scala @@ -22,6 +22,7 @@ package field import java.util.Date import scala.collection.JavaConversions._ +import scala.xml.NodeSeq import common.{Box, Empty, Failure, Full} import json.JsonAST._ @@ -77,7 +78,7 @@ class MongoListField[OwnerType <: BsonRecord[OwnerType], ListType](rec: OwnerTyp case other => setBox(Failure("Error parsing String into a JValue: "+in)) } - def toForm = Empty // FIXME + def toForm: Box[NodeSeq] = Empty def asJValue = JArray(value.map(li => li.asInstanceOf[AnyRef] match { case x if primitive_?(x.getClass) => primitive2jvalue(x) diff --git a/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/MongoRefField.scala b/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/MongoRefField.scala new file mode 100644 index 0000000000..e5b4cc0f3d --- /dev/null +++ b/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/MongoRefField.scala @@ -0,0 +1,135 @@ +/* + * Copyright 2011 WorldWide Conferencing, LLC + * + * Licensed 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 net.liftweb +package mongodb +package record +package field + +import common.{Box, Empty, Full} +import http.SHtml +import util.Helpers._ + +import java.util.UUID + +import org.bson.types.ObjectId +import net.liftweb.record.TypedField +import net.liftweb.record.field._ + +/* + * Trait for creating a Field for storing a "foreign key". Caches the + * item after fetching. Implementations are available for ObjectId, UUID, String, + * Int, and Long, but you can mix this into any Field. + * + * toForm produces a select form element. You just need to supply the + * options by overriding the options method. + */ +trait MongoRefField[RefType <: MongoRecord[RefType], MyType] extends TypedField[MyType] { + + /** The MongoMetaRecord of the referenced object **/ + def refMeta: MongoMetaRecord[RefType] + + /* + * get the referenced object + */ + def obj = synchronized { + if (!_calcedObj) { + _calcedObj = true + this._obj = valueBox.flatMap(v => refMeta.findAny(v)) + } + _obj + } + + def cached_? : Boolean = synchronized { _calcedObj } + + def primeObj(obj: Box[RefType]) = synchronized { + _obj = obj + _calcedObj = true + } + + private var _obj: Box[RefType] = Empty + private var _calcedObj = false + + /** Options for select list **/ + def options: List[(Box[MyType], String)] = Nil + + /** Label for the selection item representing Empty, show when this field is optional. Defaults to the empty string. */ + def emptyOptionLabel: String = "" + + def buildDisplayList: List[(Box[MyType], String)] = { + if (optional_?) (Empty, emptyOptionLabel)::options else options + } + + private def elem = SHtml.selectObj[Box[MyType]]( + buildDisplayList, + Full(valueBox), + setBox(_) + ) % ("tabindex" -> tabIndex.toString) + + override def toForm = + if (options.length > 0) + uniqueFieldId match { + case Full(id) => Full(elem % ("id" -> id)) + case _ => Full(elem) + } + else + Empty +} + +class ObjectIdRefField[OwnerType <: BsonRecord[OwnerType], RefType <: MongoRecord[RefType]]( + rec: OwnerType, rm: MongoMetaRecord[RefType] +) + extends ObjectIdField[OwnerType](rec) + with MongoRefField[RefType, ObjectId] +{ + def refMeta = rm +} + +class UUIDRefField[OwnerType <: BsonRecord[OwnerType], RefType <: MongoRecord[RefType]]( + rec: OwnerType, rm: MongoMetaRecord[RefType] +) + extends UUIDField[OwnerType](rec) + with MongoRefField[RefType, UUID] +{ + def refMeta = rm +} + +class StringRefField[OwnerType <: BsonRecord[OwnerType], RefType <: MongoRecord[RefType]]( + rec: OwnerType, rm: MongoMetaRecord[RefType], maxLen: Int +) + extends StringField[OwnerType](rec, maxLen) + with MongoRefField[RefType, String] +{ + def refMeta = rm +} + +class IntRefField[OwnerType <: BsonRecord[OwnerType], RefType <: MongoRecord[RefType]]( + rec: OwnerType, rm: MongoMetaRecord[RefType] +) + extends IntField[OwnerType](rec) + with MongoRefField[RefType, Int] +{ + def refMeta = rm +} + +class LongRefField[OwnerType <: BsonRecord[OwnerType], RefType <: MongoRecord[RefType]]( + rec: OwnerType, rm: MongoMetaRecord[RefType] +) + extends LongField[OwnerType](rec) + with MongoRefField[RefType, Long] +{ + def refMeta = rm +} diff --git a/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/Fixtures.scala b/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/Fixtures.scala index cb0b391ade..34e251df45 100644 --- a/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/Fixtures.scala +++ b/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/Fixtures.scala @@ -14,10 +14,10 @@ * limitations under the License. */ -package net.liftweb -package mongodb -package record -package fixtures +package net.liftweb +package mongodb +package record +package fixtures import field._ @@ -133,7 +133,7 @@ class FieldTypeTestRecord private () extends MongoRecord[FieldTypeTestRecord] wi object optionalTimeZoneField extends OptionalTimeZoneField(this) override def equals(other: Any): Boolean = other match { - case that:FieldTypeTestRecord => + case that: FieldTypeTestRecord => this.id.value == that.id.value && //this.mandatoryBinaryField.value == that.mandatoryBinaryField.value && this.mandatoryBooleanField.value == that.mandatoryBooleanField.value && @@ -198,7 +198,7 @@ class MongoFieldTypeTestRecord private () extends MongoRecord[MongoFieldTypeTest object legacyOptionalUUIDField extends UUIDField(this) { override def optional_? = true } override def equals(other: Any): Boolean = other match { - case that:MongoFieldTypeTestRecord => + case that: MongoFieldTypeTestRecord => this.id.value == that.id.value && this.mandatoryDateField.value == that.mandatoryDateField.value && this.mandatoryJsonObjectField.value == that.mandatoryJsonObjectField.value && @@ -238,7 +238,7 @@ class ListTestRecord private () extends MongoRecord[ListTestRecord] with UUIDPk[ // TODO: More List types override def equals(other: Any): Boolean = other match { - case that:ListTestRecord => + case that: ListTestRecord => this.id.value == that.id.value && this.mandatoryStringListField.value == that.mandatoryStringListField.value && this.mandatoryIntListField.value == that.mandatoryIntListField.value && @@ -250,7 +250,7 @@ object ListTestRecord extends ListTestRecord with MongoMetaRecord[ListTestRecord override def formats = allFormats } -class MapTestRecord extends MongoRecord[MapTestRecord] with StringPk[MapTestRecord] { +class MapTestRecord private () extends MongoRecord[MapTestRecord] with StringPk[MapTestRecord] { def meta = MapTestRecord object mandatoryStringMapField extends MongoMapField[MapTestRecord, String](this) @@ -262,7 +262,7 @@ class MapTestRecord extends MongoRecord[MapTestRecord] with StringPk[MapTestReco // TODO: More Map types, including JsonObject (will require a new Field type) override def equals(other: Any): Boolean = other match { - case that:MapTestRecord => + case that: MapTestRecord => this.id.value == that.id.value && this.mandatoryStringMapField.value == that.mandatoryStringMapField.value && this.mandatoryIntMapField.value == that.mandatoryIntMapField.value @@ -290,7 +290,7 @@ object LifecycleTestRecord extends LifecycleTestRecord with MongoMetaRecord[Life /* * SubRecord fields */ -class SubRecord extends BsonRecord[SubRecord] { +class SubRecord private () extends BsonRecord[SubRecord] { def meta = SubRecord object name extends StringField(this, 12) @@ -304,7 +304,7 @@ class SubRecord extends BsonRecord[SubRecord] { object uuid extends UUIDField(this) override def equals(other: Any): Boolean = other match { - case that:SubRecord => this.toString == that.toString + case that: SubRecord => this.toString == that.toString case _ => false } } @@ -312,13 +312,13 @@ object SubRecord extends SubRecord with BsonMetaRecord[SubRecord] { override def formats = allFormats } -class SubSubRecord extends BsonRecord[SubSubRecord] { +class SubSubRecord private () extends BsonRecord[SubSubRecord] { def meta = SubSubRecord object name extends StringField(this, 12) override def equals(other: Any): Boolean = other match { - case that:SubSubRecord => + case that: SubSubRecord => this.name.value == that.name.value case _ => false } @@ -327,7 +327,7 @@ object SubSubRecord extends SubSubRecord with BsonMetaRecord[SubSubRecord] { override def formats = allFormats } -class SubRecordTestRecord extends MongoRecord[SubRecordTestRecord] with ObjectIdPk[SubRecordTestRecord] { +class SubRecordTestRecord private () extends MongoRecord[SubRecordTestRecord] with ObjectIdPk[SubRecordTestRecord] { def meta = SubRecordTestRecord object mandatoryBsonRecordField extends BsonRecordField(this, SubRecord) @@ -341,7 +341,7 @@ class SubRecordTestRecord extends MongoRecord[SubRecordTestRecord] with ObjectId } override def equals(other: Any): Boolean = other match { - case that:SubRecordTestRecord => this.toString == that.toString + case that: SubRecordTestRecord => this.toString == that.toString case _ => false } @@ -355,7 +355,7 @@ case class JsonObj(id: String, name: String) extends JsonObject[JsonObj] { } object JsonObj extends JsonObjectMeta[JsonObj] -class NullTestRecord extends MongoRecord[NullTestRecord] with IntPk[NullTestRecord] { +class NullTestRecord private () extends MongoRecord[NullTestRecord] with IntPk[NullTestRecord] { def meta = NullTestRecord @@ -367,6 +367,11 @@ class NullTestRecord extends MongoRecord[NullTestRecord] with IntPk[NullTestReco def defaultValue = JsonObj("1", null) } object jsonobjlist extends MongoJsonObjectListField[NullTestRecord, JsonObj](this, JsonObj) + + override def equals(other: Any): Boolean = other match { + case that: NullTestRecord => this.toString == that.toString + case _ => false + } } object NullTestRecord extends NullTestRecord with MongoMetaRecord[NullTestRecord] @@ -377,15 +382,36 @@ extends JsonObject[BoxTestJsonObj] { } object BoxTestJsonObj extends JsonObjectMeta[BoxTestJsonObj] -class BoxTestRecord extends MongoRecord[BoxTestRecord] with LongPk[BoxTestRecord] { +class BoxTestRecord private () extends MongoRecord[BoxTestRecord] with LongPk[BoxTestRecord] { def meta = BoxTestRecord object jsonobj extends JsonObjectField[BoxTestRecord, BoxTestJsonObj](this, BoxTestJsonObj) { def defaultValue = BoxTestJsonObj("0", Empty, Full("Full String"), Failure("Failure")) } object jsonobjlist extends MongoJsonObjectListField[BoxTestRecord, BoxTestJsonObj](this, BoxTestJsonObj) + + override def equals(other: Any): Boolean = other match { + case that: BoxTestRecord => this.toString == that.toString + case _ => false + } } object BoxTestRecord extends BoxTestRecord with MongoMetaRecord[BoxTestRecord] { override def formats = super.formats + new JsonBoxSerializer } +/* + * MongoRefFields + */ +class RefFieldTestRecord private () extends MongoRecord[RefFieldTestRecord] with ObjectIdPk[RefFieldTestRecord] { + def meta = RefFieldTestRecord + + object mandatoryObjectIdRefField extends ObjectIdRefField(this, FieldTypeTestRecord) + object mandatoryUUIDRefField extends UUIDRefField(this, ListTestRecord) + object mandatoryStringRefField extends StringRefField(this, MapTestRecord, 100) + object mandatoryIntRefField extends IntRefField(this, NullTestRecord) + object mandatoryLongRefField extends LongRefField(this, BoxTestRecord) +} + +object RefFieldTestRecord extends RefFieldTestRecord with MongoMetaRecord[RefFieldTestRecord] { + override def formats = allFormats +} diff --git a/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/MongoRecordSpec.scala b/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/MongoRecordSpec.scala index 03f11ace18..0110d37fee 100644 --- a/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/MongoRecordSpec.scala +++ b/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/MongoRecordSpec.scala @@ -511,6 +511,32 @@ object MongoRecordSpec extends Specification("MongoRecord Specification") with M } } + "retrieve MongoRef objects properly" in { + checkMongoIsRunning + + val ntr = NullTestRecord.createRecord + val btr = BoxTestRecord.createRecord + + fttr.save + ltr.save + mtr.save + ntr.save + btr.save + + val rftr = RefFieldTestRecord.createRecord + .mandatoryObjectIdRefField(fttr.id.is) + .mandatoryUUIDRefField(ltr.id.is) + .mandatoryStringRefField(mtr.id.is) + .mandatoryIntRefField(ntr.id.is) + .mandatoryLongRefField(btr.id.is) + + rftr.mandatoryObjectIdRefField.obj mustEqual Full(fttr) + rftr.mandatoryUUIDRefField.obj mustEqual Full(ltr) + rftr.mandatoryStringRefField.obj mustEqual Full(mtr) + rftr.mandatoryIntRefField.obj mustEqual Full(ntr) + rftr.mandatoryLongRefField.obj mustEqual Full(btr) + } + } }