/*
* Copyright 2007-2008 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.record
import net.liftweb._
import util._
import scala.collection.mutable.{ListBuffer}
import scala.xml._
import net.liftweb.http.js.{JsExp, JE, JsObj}
import net.liftweb.http.{FieldError, SHtml, Req, LiftResponse, LiftRules}
import _root_.java.lang.reflect.Method
import field._
import Box._
import JE._
import Helpers._
/**
* Holds meta information and operations on a record
*/
trait MetaRecord[BaseRecord <: Record[BaseRecord]] {
self: BaseRecord =>
private var fieldList: List[FieldHolder] = Nil
private var lifecycleCallbacks: List[(String, Method)] = Nil
/**
* Set this to use your own form template when rendering a Record to a form.
*
* This template is any given XHtml that contains three nodes acting as placeholders such as:
*
* <pre>
*
* <lift:field_label name="firstName"/> - the label for firstName field will be rendered here
* <lift:field name="firstName"/> - the firstName field will be rendered here (typically an input field)
* <lift:field_msg name="firstName"/> - the <lift:msg> will be rendered here hafing the id given by
* uniqueFieldId of the firstName field.
*
*
* Example.
*
* Having:
*
* class MyRecord extends Record[MyRecord] {
*
* def meta = MyRecordMeta
*
* object firstName extends StringField(this, "John")
*
* }
*
* object MyRecordMeta extends MyRecord with MetaRecord[MyRecord] {
* override def mutable_? = false
* }
*
* ...
*
* val rec = MyRecordMeta.createRecord.firstName("McLoud")
*
* val template =
* <div>
* <div>
* <div><lift:field_label name="firstName"/></div>
* <div><lift:field name="firstName"/></div>
* <div><lift:field_msg name="firstName"/></div>
* </div>
* </div>
*
* MyRecordMeta.formTemplate = Full(template)
* rec.toForm((r:MyRecord) => println(r));
*
* </pre>
*
*/
var formTemplate: Box[NodeSeq] = Empty
protected val rootClass = this.getClass.getSuperclass
private def isLifecycle(m: Method) = classOf[LifecycleCallbacks].isAssignableFrom(m.getReturnType)
private def isField(m: Method) = classOf[Field[_, _]].isAssignableFrom(m.getReturnType)
def introspect(rec: BaseRecord, methods: Array[Method])(f: (Method, OwnedField[BaseRecord]) => Any) = {
for (v <- methods if isField(v)) {
v.invoke(rec) match {
case mf: OwnedField[BaseRecord] if !mf.ignoreField_? =>
mf.setName_!(v.getName)
f(v, mf)
case _ =>
}
}
}
this.runSafe {
val tArray = new ListBuffer[FieldHolder]
lifecycleCallbacks = for (v <- rootClass.getMethods.toList
if isLifecycle(v)) yield (v.getName, v)
introspect(this, rootClass.getMethods) {
case (v, mf) => tArray += FieldHolder(mf.name, v, mf)
}
def findPos(in: AnyRef): Box[Int] = {
tArray.toList.zipWithIndex.filter(mft => in eq mft._1.field) match {
case Nil => Empty
case x :: xs => Full(x._2)
}
}
val resArray = new ListBuffer[FieldHolder]
fieldOrder.foreach(f => findPos(f).foreach(pos => resArray += tArray.remove(pos)))
tArray.foreach(mft => resArray += mft)
fieldList = resArray.toList
}
/**
* Specifies if this Record is mutable or not
*/
def mutable_? = true
/**
* Creates a mew record
*/
def createRecord: BaseRecord = {
val rec: BaseRecord = rootClass.newInstance.asInstanceOf[BaseRecord]
rec.runSafe {
introspect(rec, rec.getClass.getMethods) {case (v, mf) =>}
}
rec
}
/**
* Creates a mew record from a JSON construct
*
* @param json - the stringified JSON stucture
*/
def createRecord(json: String): Box[BaseRecord] = {
val rec: BaseRecord = rootClass.newInstance.asInstanceOf[BaseRecord]
rec.runSafe {
introspect(rec, rec.getClass.getMethods) {case (v, mf) =>}
}
rec.fromJSON(json)
}
/**
* Creates a new record setting the value of the fields from the original object but
* apply the new value for the specific field
*
* @param - original the initial record
* @param - field the new mutated field
* @param - the new value of the field
*/
def createWithMutableField[FieldType](original: BaseRecord,
field: Field[FieldType, BaseRecord],
newValue: FieldType): BaseRecord = {
val rec = createRecord
for (f <- fieldList) {
if (f.name == field.name)
fieldByName(f.name, rec).map(recField => recField.set(newValue.asInstanceOf[recField.MyType]) )
else
fieldByName(f.name, rec).map(recField =>
fieldByName(f.name, original).map(m => recField.setFromAny(m.value))
)
}
rec
}
/**
* Returns the HTML representation of inst Record.
*
* @param inst - th designated Record
* @return a NodeSeq
*/
def toXHtml(inst: BaseRecord): NodeSeq = fieldList.flatMap(holder =>
fieldByName(holder.name, inst).map(_.toXHtml).openOr(NodeSeq.Empty) ++ Text("\n"))
/**
* Validates the inst Record by calling validators for each field
*
* @pram inst - the Record tobe validated
* @return a List of FieldError. If this list is empty you can assume that record was validated successfully
*/
def validate(inst: BaseRecord): List[FieldError] = {
foreachCallback(inst, _.beforeValidation)
try{
fieldList.flatMap(holder => inst.fieldByName(holder.name) match {
case Full(field) => if (!field.valueCouldNotBeSet) {
field.validators.flatMap(_(field.value).map(FieldError(field, _)))
} else {
FieldError(field, Text(field.noValueErrorMessage)) :: Nil
}
case _ => Nil
})
} finally {
foreachCallback(inst, _.afterValidation)
}
}
/**
* Returns the JSON representation of <i>inst</i> record
*
* @param inst: BaseRecord
* @return JsObj
*/
def asJSON(inst: BaseRecord): JsObj = {
JsObj((for (holder <- fieldList;
field <- inst.fieldByName(holder.name)) yield (field.name, field.asJs)):_*)
}
/**
* Populate the inst's fields with the values from the JSON construct
*
* @param inst - the record that will be populated
* @param json - The stringified JSON object
* @return Box[BaseRecord]
*/
def fromJSON(inst: BaseRecord, json: String): Box[BaseRecord] = {
JSONParser.parse(json) match {
case Full(nvp : Map[_, _]) =>
for ((k, v) <- nvp;
field <- inst.fieldByName(k.toString)) yield field.setFromAny(v)
Full(inst)
case _ => Empty
}
}
private[record] def foreachCallback(inst: BaseRecord, f: LifecycleCallbacks => Any) {
lifecycleCallbacks.foreach(m => f(m._2.invoke(inst).asInstanceOf[LifecycleCallbacks]))
}
/**
* Returns the XHTML representation of inst Record. If formTemplate is set,
* this template will be used otherwise a default template is considered.
*
* @param inst - the record to be rendered
* @return the XHTML content as a NodeSeq
*/
def toForm(inst: BaseRecord): NodeSeq = {
formTemplate match {
case Full(template) => toForm(inst, template)
case _ => fieldList.flatMap(holder => fieldByName(holder.name, inst).
map(_.toForm).openOr(NodeSeq.Empty) ++ Text("\n"))
}
}
/**
* Returns the XHTML representation of inst Record. You must provide the Node template
* to represent this record in the proprietary layout.
*
* @param inst - the record to be rendered
* @param template - The markup template forthe form. See also the formTemplate variable
* @return the XHTML content as a NodeSeq
*/
def toForm(inst: BaseRecord, template: NodeSeq): NodeSeq = {
template match {
case e @ <lift:field_label>{_*}</lift:field_label> => e.attribute("name") match {
case Some(name) => fieldByName(name.toString, inst).map(_.label).openOr(NodeSeq.Empty)
case _ => NodeSeq.Empty
}
case e @ <lift:field>{_*}</lift:field> => e.attribute("name") match {
case Some(name) => fieldByName(name.toString, inst).map(_.asXHtml).openOr(NodeSeq.Empty)
case _ => NodeSeq.Empty
}
case e @ <lift:field_msg>{_*}</lift:field_msg> => e.attribute("name") match {
case Some(name) => fieldByName(name.toString, inst).map(_.uniqueFieldId match {
case Full(id) => <lift:msg id={id}/>
case _ => NodeSeq.Empty
}).openOr(NodeSeq.Empty)
case _ => NodeSeq.Empty
}
case Elem(namespace, label, attrs, scp, ns @ _*) =>
Elem(namespace, label, attrs, scp, toForm(inst, ns.flatMap(n => toForm(inst, n))):_* )
case s : Seq[_] => s.flatMap(e => e match {
case Elem(namespace, label, attrs, scp, ns @ _*) =>
Elem(namespace, label, attrs, scp, toForm(inst, ns.flatMap(n => toForm(inst, n))):_* )
case x => x
})
}
}
private def ??(meth: Method, inst: BaseRecord) = meth.invoke(inst).asInstanceOf[OwnedField[BaseRecord]]
/**
* Get a field by the field name
* @param fieldName -- the name of the field to get
* @param actual -- the instance to get the field on
*
* @return Box[The Field] (Empty if the field is not found)
*/
def fieldByName(fieldName: String, inst: BaseRecord): Box[OwnedField[BaseRecord]] = {
Box(fieldList.find(f => f.name == fieldName)).
map(holder => ??(holder.method, inst).asInstanceOf[OwnedField[BaseRecord]])
}
/**
* Prepend a DispatchPF function to LiftRules.dispatch. If the partial function id defined for a give Req
* it will construct a new Record based on the HTTP query string parameters
* and will pass this Record to the function returned by func parameter.
*
* @param func - a PartialFunction for associating a request with a user provided function and the proper Record
*/
def prependDispatch(func: PartialFunction[Req, BaseRecord => Box[LiftResponse]])= {
LiftRules.dispatch.prepend (makeFunc(func))
}
/**
* Append a DispatchPF function to LiftRules.dispatch. If the partial function id defined for a give Req
* it will construct a new Record based on the HTTP query string parameters
* and will pass this Record to the function returned by func parameter.
*
* @param func - a PartialFunction for associating a request with a user provided function and the proper Record
*/
def appendDispatch(func: PartialFunction[Req, BaseRecord => Box[LiftResponse]])= {
LiftRules.dispatch.append (makeFunc(func))
}
private def makeFunc(func: PartialFunction[Req, BaseRecord => Box[LiftResponse]]) = new PartialFunction[Req, () => Box[LiftResponse]] {
def isDefinedAt(r: Req): Boolean = func.isDefinedAt(r)
def apply(r: Req): () => Box[LiftResponse] = {
val rec = fromReq(r)
() => func(r)(rec)
}
}
def fromReq(r: Req): BaseRecord = {
val rec = createRecord
for(fieldHolder <- fieldList;
field <- rec.fieldByName(fieldHolder.name)
) yield {
field.setFromAny(r.param(field.name))
}
rec
}
/**
* Defined the order of the fields in this record
*
* @return a List of Field
*/
def fieldOrder: List[OwnedField[BaseRecord]] = Nil
protected def fields() : List[OwnedField[BaseRecord]] = fieldList map (fh => fh.field)
case class FieldHolder(name: String, method: Method, field: OwnedField[BaseRecord])
}
trait LifecycleCallbacks {
def beforeValidation {}
def afterValidation {}
def beforeSave {}
def beforeCreate {}
def beforeUpdate {}
def afterSave {}
def afterCreate {}
def afterUpdate {}
def beforeDelete {}
def afterDelete {}
}