![JAR search and dependency download from the Maven repository](/logo.png)
sjson.json.JsBean.scala Maven / Gradle / Ivy
package sjson
package json
import java.lang.reflect.Modifier
import java.util.TimeZone
trait JsBean {
implicit def string2Class[T<:AnyRef](name: String)(implicit classLoader: ClassLoader): Class[T] = {
val clazz = Class.forName(name, true, classLoader)
clazz.asInstanceOf[Class[T]]
}
private [json] def lookupType[T](parent: Class[T], name: String): Class[_] = {
parent.getDeclaredField(name).getType
}
class NiceObject[T <: AnyRef](x : T) {
def niceClass : Class[_ <: T] = x.getClass.asInstanceOf[Class[T]]
}
implicit def toNiceObject[T <: AnyRef](x : T) = new NiceObject(x)
import java.beans._
import java.lang.reflect.Field
import dispatch.json._
import Util._
private def getProps[T](clazz: Class[T]) = {
val fields = clazz.getMethods
Map() ++
fields.map {field =>
val a = field.getAnnotation(classOf[JSONProperty])
a match {
case null => (field.getName, field.getName)
case x if x.value.length > 0 =>
(x.value, field.getName)
case x => (field.getName, field.getName)
}
}
}
private def getInnerTypeForOption[T](clazz: Class[T], field: Field) = {
if (clazz.isAssignableFrom(classOf[Option[_]])) {
// get the inner type from the OptionTypeHint annotation
val an = field.getAnnotation(classOf[OptionTypeHint])
an match {
case null =>
throw new IllegalArgumentException("cannot get type information")
case _ =>
an.value
}
} else { clazz }
}
private def processMap(m: Map[_,_], field: Field) = {
val ann = field.getAnnotation(classOf[JSONTypeHint])
(Some(field),
Map() ++
(ann match {
case null =>
m.map {case (y1: JsValue, y2: JsValue) =>
(y1.self, y2.self)
}
case x if x.value.isPrimitive == true =>
// remember all numbers are converted to BigDecimal by the JSON parser
m.map {case (y1: JsValue, y2: JsValue) =>
if (y2.isInstanceOf[JsNumber]) (y1.self, mkNum(y2.self.asInstanceOf[BigDecimal], ann.value))
else (y1.self, y2.self)
}
case _ =>
m.map {case (y1: JsValue, y2: JsValue) =>
(y1.self, fromJSON(y2, Some(ann.value), field))
}
}))
}
private def processTuple2(t: Tuple2[_,_], field: Field) = {
val (t1: JsValue, t2: JsValue) = t
val ann = field.getAnnotation(classOf[JSONTypeHint])
(Some(field),
(ann match {
case null =>
(t1.self, t2.self)
case x if x.value.isPrimitive == true =>
// remember all numbers are converted to BigDecimal by the JSON parser
if (t2.isInstanceOf[JsNumber]) (t1.self, mkNum(t2.self.asInstanceOf[BigDecimal], ann.value))
else (t1.self, t2.self)
case _ =>
(t1.self, fromJSON(t2, Some(ann.value), field))
}))
}
/**
* Convert the value to an Enumeration.Value instance using class enumObjectClass's valueOf method. Returns an instance of
* Enumeration.Value.
*/
private def toEnumValue[T](value: Any, enumObjectClass: Class[T]): Enumeration#Value = {
if (Modifier.isAbstract(enumObjectClass.getModifiers)) {
throw new IllegalArgumentException("cannot get type information for enum " + value)
}
val method = enumObjectClass.getMethod("withName", classOf[String])
method.invoke(null, value.asInstanceOf[String]).asInstanceOf[Enumeration#Value]
}
private def getEnumObjectClass[T <: Enumeration#Value](targetClass: Class[T], y: Field): Class[_] = {
val enumObjectClass = y.getAnnotation(classOf[EnumTypeHint]) match {
case null =>
targetClass.getEnclosingClass
case an => Class.forName(an.value)
}
enumObjectClass
}
/**
* Convert the JsValue to an instance of the class context, using the parent for any annotation hints.
* Returns an instance of T.
*/
private[json] def fromJSON[T](js: JsValue, context: Option[Class[T]], parent: Field): T = {
if (context.isDefined && classOf[Enumeration#Value].isAssignableFrom(context.get)) {
toEnumValue(js.self, getEnumObjectClass(context.get.asInstanceOf[Class[Enumeration#Value]], parent)).asInstanceOf[T]
} else {
fromJSON(js, context)
}
}
/**
* Convert the JsValue to an instance of the class context. Returns an instance of
* T.
*/
def fromJSON[T](js: JsValue, context: Option[Class[T]]): T = {
if (!js.isInstanceOf[JsObject] || !context.isDefined) js.self.asInstanceOf[T]
else {
// bean as a map from json
val bean = js.self.asInstanceOf[Map[JsString, JsValue]]
// properties of the bean class
// as a map to take care of mappings for JSONProperty annotation
val props = getProps(context.get)
// iterate on name/value pairs of the bean
val info = bean map {case (JsString(name), value) =>
value.self match {
// need to ignore properties in json that are not in props
case x if (props.get(name).isDefined == false) =>
(None, null)
/**
* Can be a Map in any of the following cases:
* 1. the data member is really a scala.Collection.Map
* 2. tha data member is a Tuple2 which also we serialize as a Map
* 3. the data member can be an object which comes in JSON as a Map
*/
case x: Map[_, _] => {
// type of the property from the bean class
val cl = lookupType(context.get, props.get(name).get)
// field
val field = context.get.getDeclaredField(props.get(name).get)
// can be an Option[_]
val inner = getInnerTypeForOption(cl, field)
inner match {
// case 1
case m if (m isAssignableFrom(classOf[Map[_,_]])) =>
processMap(x, field)
// case 2
case t if (t isAssignableFrom(classOf[Tuple2[_,_]])) =>
processTuple2(x.toList.head, field)
// case 3
case _ =>
(Some(field), fromJSON(value, Some(inner), field))
}
}
case x: List[_] => {
val field = context.get.getDeclaredField(props.get(name).get)
// empty list as value and type = Option means None
// if (field.getType.isAssignableFrom(classOf[Option[_]]) && x.isEmpty)
// (Some(field), None)
// else {
val ann = field.getAnnotation(classOf[JSONTypeHint])
ann match {
case null =>
(Some(field),
x.map{ case y: JsValue => y.self
})
case a if a.value.isPrimitive == true =>
(Some(field),
x.map{case y: JsValue =>
// remember all numbers are converted to BigDecimal by the JSON parser
if (y.isInstanceOf[JsNumber]) mkNum(y.self.asInstanceOf[BigDecimal], ann.value)
else y.self
})
case _ =>
(Some(field),
x.map{ case y: JsValue => fromJSON(y, Some(ann.value), field)
})
}
// }
}
case x =>
(Some(context.get.getDeclaredField(props.get(name).get)), value.self)
}
}
newInstance(context.get) { instance =>
info.foreach {x =>
x match {
case (None, _) =>
case (Some(y), z) => {
y.setAccessible(true)
// type conversion hacks
val num =
// json parser makes BigDecimal out of all numbers
if (z.isInstanceOf[BigDecimal]) mkNum(z.asInstanceOf[BigDecimal], y.getType)
// if it's timezone, need to make one from JSON string
else if (y.getType.isAssignableFrom(classOf[java.util.TimeZone])) TimeZone.getTimeZone(z.asInstanceOf[String])
// if it's date, need to make one from JSON string
else if (y.getType.isAssignableFrom(classOf[java.util.Date])) mkDate(z.asInstanceOf[String])
// process Enumerations
else if (classOf[Enumeration#Value].isAssignableFrom(y.getType)) {
toEnumValue(z, getEnumObjectClass(y.getType.asInstanceOf[Class[Enumeration#Value]], y))
}
// as ugly as it gets
else if (y.getType.isArray) {
mkArray(z.asInstanceOf[List[_]], y.getType.getComponentType)
}
// special treatment for JSON "nulls"
// else if (z.isInstanceOf[String] && (z == "null")) null
else if (z.isInstanceOf[String] && (z == null)) null
else z
// need to handle Option[] in individual fields
if (y.getType.isAssignableFrom(classOf[scala.Option[_]])) {
// handle None case which comes as an empty List since we serialize None as []
if (num.isInstanceOf[List[_]] && num.asInstanceOf[List[_]].isEmpty) y.set(instance, None)
else y.set(instance, Some(num))
} else {
y.set(instance, num)
}
}
}
}
}
}
}
private def mkArray(l: List[_], clz: Class[_]): Array[_] = {
import java.lang.reflect.{Array => JArray}
val a = JArray.newInstance(clz, l.size)
var i = 0
while (i < l.size) {
JArray.set(a, i, l(i))
i += 1
}
a.asInstanceOf[Array[_]]
}
/**
* Generate a JSON representation of the object obj and return the string.
*/
def toJSON[T <: AnyRef](obj: T): String = obj match {
// case null => quote("null")
case null => "null"
case (n: Number) => obj.toString
case (b: java.lang.Boolean) => obj.toString
case (s: String) => quote(obj.asInstanceOf[String])
case (d: java.util.Date) =>
quote(obj.asInstanceOf[java.util.Date].getTime.toString)
case (d: java.util.TimeZone) => quote(d.getID)
case (v: Enumeration#Value) =>
quote(v toString)
case (s: Seq[AnyRef]) =>
s.map(e => toJSON(e)).mkString("[", ",", "]")
case (s: Array[AnyRef]) =>
s.map(e => toJSON(e)).mkString("[", ",", "]")
case (m: Map[AnyRef, AnyRef]) =>
m.map(e => toJSON(e._1.toString) + ":" + toJSON(e._2))
.mkString("{", ",", "}")
case (t: Tuple2[AnyRef, AnyRef]) =>
"{" + toJSON(t._1) + ":" + toJSON(t._2) + "}"
case _ => {
// handle beans
val clazz = obj.niceClass
// just an observation:
// if the class is not at the top most level, then annotating the class
// with @BeanInfo does not work. Need to annotate every property with @BeanProperty
val pds =
Introspector.getBeanInfo(clazz)
.getPropertyDescriptors
.filter(_.getName != "class")
if (pds.isEmpty) {
throw new UnsupportedOperationException("Class " + clazz + " not supported for conversion")
}
val props =
for {
pd <- pds
val rm = pd.getReadMethod
val rv = rm.invoke(obj)
// Option[] needs to be treated differently
val (rval, isOption) = rv match {
case (o: Option[_]) =>
if (o.isDefined) (o.get.asInstanceOf[AnyRef], true) else (List(), true) // serialize None as []
case x => (x, false)
}
val ann = rm.getAnnotation(classOf[JSONProperty])
val v =
if (ann == null || ann.value == null || ann.value.length == 0) pd.getName
else ann.value
val ignore =
if (ann != null) ann.ignore || (rv == null && ann.ignoreIfNull) else false
if ((ignore == false) && (!isOption || (isOption && rval != null)))
} yield toJSON(v) + ":" + toJSON(rval)
props.mkString("{", ",", "}")
}
}
def newInstance[T](clazz: Class[T])(op: T => Unit): T
}
/**
* Use this trait with JsBean to instantiate classes using a default private constructor. This is the default.
*/
trait DefaultConstructor {
import java.lang.reflect._
def newInstance[T](clazz: Class[T])(op: T => Unit): T = {
// need to access private default constructor .. hack!
// clazz.getDeclaredConstructors.foreach(println)
val constructor =
clazz.getDeclaredConstructors.filter(_.getParameterTypes.length == 0).head
if (!Modifier.isPublic(constructor.getModifiers()) ||
!Modifier.isPublic(constructor.getDeclaringClass().getModifiers()))
constructor.setAccessible(true)
val v = constructor.newInstance().asInstanceOf[T]
op(v)
v
}
}
object JsBean extends JsBean with DefaultConstructor
/**
* Use this trait with JsBean to instantiate classes using Objenesis.
*
* This is faster and negates the need for a default no-args constructor.
* However it adds a runtime dependency on objenesis.jar.
*
* @see http://objenesis.googlecode.com/svn/docs/index.html
* @author Joe Walnes
*/
trait Objenesis {
import org.objenesis.ObjenesisStd
val objenesis = new ObjenesisStd
def newInstance[T](clazz: Class[T])(op: T => Unit): T = {
val v = objenesis.newInstance(clazz).asInstanceOf[T]
op(v)
v
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy