
net.liftweb.couchdb.JSONRecord.scala Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2010-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 couchdb
import java.math.MathContext
import java.util.Calendar
import scala.collection.immutable.TreeSet
import scala.reflect.Manifest
import scala.xml.NodeSeq
import net.liftweb.common.{Box, Empty, Failure, Full}
import Box.{box2Iterable, option2Box}
import net.liftweb.http.js.{JsExp, JsObj}
import net.liftweb.json.JsonParser
import net.liftweb.json.JsonAST._
import net.liftweb.record.{Field, MandatoryTypedField, MetaRecord, Record}
import net.liftweb.record.RecordHelpers.jvalueToJsExp
import net.liftweb.record.FieldHelpers.expectedA
import net.liftweb.record.field._
import net.liftweb.util.ThreadGlobal
import net.liftweb.util.Helpers._
import java.util.prefs.BackingStoreException
private[couchdb] object JSONRecordHelpers {
/** Remove duplicate fields, preferring the first field seen with a given name */
def dedupe(fields: List[JField]): List[JField] = {
var seen = TreeSet.empty[String]
fields.filter {
case JField(name, _) if seen contains name => false
case JField(name, _) => { seen = seen + name; true }
}
}
}
import JSONRecordHelpers._
/** Specialized Record that can be encoded and decoded from JSON */
trait JSONRecord[MyType <: JSONRecord[MyType]] extends Record[MyType] {
self: MyType =>
private var _additionalJFields: List[JField] = Nil
/** Refines meta to require a JSONMetaRecord */
def meta: JSONMetaRecord[MyType]
/** Extra fields to add to the encoded object, such as type. Default is none (Nil) */
def fixedAdditionalJFields: List[JField] = Nil
/**
* Additional fields that are not represented by Record fields, nor are fixed additional fields.
* Default implementation is for preserving unknown fields across read/write
*/
def additionalJFields: List[JField] = _additionalJFields
/**
* Handle any additional fields that are not represented by Record fields when decoding from a JObject.
* Default implementation preserves the fields intact and returns them via additionalJFields
*/
def additionalJFields_= (fields: List[JField]): Unit = _additionalJFields = fields
/**
* Save the instance and return the instance
*/
override def saveTheRecord(): Box[MyType] = throw new BackingStoreException("JSON Records don't save themselves")
}
object JSONMetaRecord {
/** Local override to record parsing that can cause extra fields to be ignored, even if they otherwise would cause a failure */
object overrideIgnoreExtraJSONFields extends ThreadGlobal[Boolean]
/** Local override to record parsing that can cause missing fields to be ignored, even if they otherwise would cause a failure */
object overrideNeedAllJSONFields extends ThreadGlobal[Boolean]
}
/** Specialized MetaRecord that deals with JSONRecords */
trait JSONMetaRecord[BaseRecord <: JSONRecord[BaseRecord]] extends MetaRecord[BaseRecord] {
self: BaseRecord =>
/**
* Return the name of the field in the encoded JSON object. If the field implements JSONField and has overridden jsonName then
* that will be used, otherwise the record field name
*/
def jsonName(field: Field[_, BaseRecord]): String = field match {
case (jsonField: JSONField) => jsonField.jsonName openOr field.name
case _ => field.name
}
/** Whether or not extra fields in a JObject to decode is an error (false) or not (true). The default is true */
def ignoreExtraJSONFields: Boolean = true
/** Whether or not missing fields in a JObject to decode is an error (false) or not (true). The default is true */
def needAllJSONFields: Boolean = true
override def asJSON(inst: BaseRecord): JsObj = jvalueToJsExp(asJValue).asInstanceOf[JsObj]
override def setFieldsFromJSON(inst: BaseRecord, json: String): Box[Unit] =
setFieldsFromJValue(inst, JsonParser.parse(json))
/** Encode a record instance into a JValue */
override def asJValue(rec: BaseRecord): JObject = {
val recordJFields = fields(rec).map(f => JField(jsonName(f), f.asJValue))
JObject(dedupe(recordJFields ++ rec.fixedAdditionalJFields ++ rec.additionalJFields).sortWith(_.name < _.name))
}
/** Attempt to decode a JValue, which must be a JObject, into a record instance */
override def setFieldsFromJValue(rec: BaseRecord, jvalue: JValue): Box[Unit] = {
def fromJFields(jfields: List[JField]): Box[Unit] = {
import JSONMetaRecord._
val flds = fields(rec)
lazy val recordFieldNames = TreeSet(flds.map(jsonName): _*)
lazy val jsonFieldNames = TreeSet(jfields.map(_.name): _*)
lazy val optionalFieldNames = TreeSet(flds.filter(_.optional_?).map(jsonName): _*)
lazy val recordFieldsNotInJson = recordFieldNames -- jsonFieldNames -- optionalFieldNames
lazy val jsonFieldsNotInRecord = jsonFieldNames -- recordFieldNames
// If this record type has been configured to be stricter about fields, check those first
if ((overrideNeedAllJSONFields.box openOr needAllJSONFields) && !recordFieldsNotInJson.isEmpty) {
Failure("The " + recordFieldsNotInJson.mkString(", ") + " field(s) were not found, but are required.")
} else if (!(overrideIgnoreExtraJSONFields.box openOr ignoreExtraJSONFields) && !jsonFieldsNotInRecord.isEmpty) {
Failure("Field(s) " + jsonFieldsNotInRecord.mkString(", ") + " are not recognized.")
} else {
for {
jfield <- jfields
field <- flds if jsonName(field) == jfield.name
} field.setFromJValue(jfield.value)
rec.additionalJFields = jsonFieldsNotInRecord.toList map {
name => jfields.find(_.name == name).get
}
Full(())
}
}
jvalue match {
case JObject(jfields) => fromJFields(jfields)
case other => expectedA("JObject", other)
}
}
}
/** Trait for fields with JSON-specific behavior */
trait JSONField {
/** Return Full(name) to use that name in the encoded JSON object, or Empty to use the same name as in Scala. Defaults to Empty */
def jsonName: Box[String] = Empty
}
/* ****************************************************************************************************************************************** */
/** Field that contains an entire record represented as an inline object value in the final JSON */
class JSONSubRecordField[OwnerType <: JSONRecord[OwnerType], SubRecordType <: JSONRecord[SubRecordType]]
(rec: OwnerType, valueMeta: JSONMetaRecord[SubRecordType])(implicit subRecordType: Manifest[SubRecordType])
extends Field[SubRecordType, OwnerType] with MandatoryTypedField[SubRecordType]
{
def this(rec: OwnerType, value: SubRecordType)(implicit subRecordType: Manifest[SubRecordType]) = {
this(rec, value.meta)
set(value)
}
def this(rec: OwnerType, valueMeta: JSONMetaRecord[SubRecordType], value: Box[SubRecordType])
(implicit subRecordType: Manifest[SubRecordType]) = {
this(rec, valueMeta)
setBox(value)
}
def owner = rec
def asJs = asJValue
def toForm: Box[NodeSeq] = Empty // FIXME
def defaultValue = valueMeta.createRecord
def setFromString(s: String): Box[SubRecordType] = valueMeta.fromJSON(s)
def setFromAny(in: Any): Box[SubRecordType] = genericSetFromAny(in)
def asJValue: JValue = valueBox.map(_.asJValue) openOr (JNothing: JValue)
def setFromJValue(jvalue: JValue): Box[SubRecordType] = jvalue match {
case JNothing|JNull if optional_? => setBox(Empty)
case _ => setBox(valueMeta.fromJValue(jvalue))
}
}
/** Field that contains an array of some basic JSON type */
class JSONBasicArrayField[OwnerType <: JSONRecord[OwnerType], ElemType <: JValue](rec: OwnerType)(implicit elemType: Manifest[ElemType])
extends Field[List[ElemType], OwnerType] with MandatoryTypedField[List[ElemType]]
{
def this(rec: OwnerType, value: List[ElemType])(implicit elemType: Manifest[ElemType]) = { this(rec); set(value) }
def this(rec: OwnerType, value: Box[List[ElemType]])(implicit elemType: Manifest[ElemType]) = { this(rec); setBox(value) }
def owner = rec
def asJs = asJValue
def toForm: Box[NodeSeq] = Empty // FIXME
def defaultValue = Nil
def setFromString(s: String): Box[List[ElemType]] =
setBox(tryo(JsonParser.parse(s)) flatMap {
case JArray(values) => checkValueTypes(values)
case other => expectedA("JSON string with an array of " + elemType, other)
})
def setFromAny(in: Any): Box[List[ElemType]] = genericSetFromAny(in)
def checkValueTypes(in: List[JValue]): Box[List[ElemType]] =
in.find(!_.isInstanceOf[ElemType]) match {
case Some(erroneousValue) if erroneousValue != null =>
Failure("Value in input array is a " + value.getClass.getName + ", should be a " + elemType.toString)
case Some(erroneousValue) => Failure("Value in input array is null, should be a " + elemType.toString)
case None => Full(in.map(_.asInstanceOf[ElemType]))
}
def asJValue: JValue = valueBox.map(JArray) openOr (JNothing: JValue)
def setFromJValue(jvalue: JValue): Box[List[ElemType]] = jvalue match {
case JNothing|JNull if optional_? => setBox(Empty)
case JArray(values) => setBox(checkValueTypes(values))
case other => setBox(expectedA("JArray containing " + elemType.toString, other))
}
}
/** Field that contains a homogeneous array of subrecords */
class JSONSubRecordArrayField[OwnerType <: JSONRecord[OwnerType], SubRecordType <: JSONRecord[SubRecordType]]
(rec: OwnerType, valueMeta: JSONMetaRecord[SubRecordType])(implicit valueType: Manifest[SubRecordType])
extends Field[List[SubRecordType], OwnerType] with MandatoryTypedField[List[SubRecordType]]
{
def this(rec: OwnerType, value: List[SubRecordType])(implicit subRecordType: Manifest[SubRecordType]) = {
this(rec, value.head.meta)
set(value)
}
def this(rec: OwnerType, valueMeta: JSONMetaRecord[SubRecordType], value: Box[List[SubRecordType]])
(implicit subRecordType: Manifest[SubRecordType]) = {
this(rec, valueMeta)
setBox(value)
}
def owner = rec
def asJs = asJValue
def toForm: Box[NodeSeq] = Empty // FIXME
def defaultValue = Nil
def setFromString(s: String): Box[List[SubRecordType]] =
setBox(tryo(JsonParser.parse(s)) flatMap {
case JArray(values) => fromJValues(values)
case other => expectedA("JSON string containing an array of " + valueMeta.getClass.getSuperclass.getName, other)
})
def setFromAny(in: Any): Box[List[SubRecordType]] = genericSetFromAny(in)
private def fromJValues(jvalues: List[JValue]): Box[List[SubRecordType]] =
jvalues
.foldLeft[Box[List[SubRecordType]]](Full(Nil)) {
(prev, cur) => prev.flatMap {
rest => valueMeta.fromJValue(cur).map(_::rest)
}
}
.map(_.reverse)
def asJValue: JArray = JArray(value.map(_.asJValue))
def setFromJValue(jvalue: JValue): Box[List[SubRecordType]] = jvalue match {
case JNothing|JNull if optional_? => setBox(Empty)
case JArray(jvalues) => setBox(fromJValues(jvalues))
case other => setBox(expectedA("JArray containing " + valueMeta.getClass.getSuperclass.getName, other))
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy