diff --git a/genson-scala/src/main/scala/com/owlike/genson/ScalaBeanPropertyFactory.scala b/genson-scala/src/main/scala/com/owlike/genson/ScalaBeanPropertyFactory.scala new file mode 100644 index 00000000..f634a008 --- /dev/null +++ b/genson-scala/src/main/scala/com/owlike/genson/ScalaBeanPropertyFactory.scala @@ -0,0 +1,159 @@ +package com.owlike.genson + +import com.owlike.genson.reflect._ +import java.lang.reflect.{Constructor, Method, Type, Field} +import com.owlike.genson.reflect.PropertyAccessor.{MethodAccessor, FieldAccessor} +import com.owlike.genson.reflect.BeanCreator.{MethodBeanCreator, ConstructorBeanCreator} +import com.owlike.genson.reflect.PropertyMutator.{MethodMutator, FieldMutator} +import scala.reflect.runtime.{universe => u} + +private object ScalaReflectionApiLock + +// its a bit ugly to do all this only to handle erased primitive types in scala, but it works to some degree... +private[genson] class ScalaBeanPropertyFactory extends BeanPropertyFactory { + + val mirror = ScalaReflectionApiLock.synchronized { + u.runtimeMirror(classOf[ScalaBundle].getClassLoader) + } + + def createAccessor(name: String, field: Field, ofType: Type, genson: Genson): PropertyAccessor = { + fieldType(field, ofType) + .map(new FieldAccessor(name, field, _, field.getDeclaringClass)) + .getOrElse(null) + } + + def createAccessor(name: String, method: Method, ofType: Type, genson: Genson): PropertyAccessor = { + ScalaReflectionApiLock.synchronized { + val sType = expandToJavaType(matchingMethod(method).returnType, ofType) + sType.map(new MethodAccessor(name, method, _, method.getDeclaringClass)) + .getOrElse(null) + } + } + + def createCreator(ofType: Type, ctr: Constructor[_], resolvedNames: Array[String], genson: Genson): BeanCreator = { + ScalaReflectionApiLock.synchronized { + val matchingCtrs = mirror.classSymbol(ctr.getDeclaringClass) + .selfType + .declaration(u.nme.CONSTRUCTOR) + .asTerm + .alternatives + .filter(p => p.asMethod.paramss.flatten.length == ctr.getParameterTypes.length) + + if (matchingCtrs.length != 1) + throw new UnsupportedOperationException( + "Failed to match single constructor, please report this issue." + ) + + val sCtr = matchingCtrs.head.asMethod + val parameterTypes = sCtr.paramss + .flatten + .flatMap(p => expandToJavaType(p.typeSignature, ofType)) + .toArray + + if (parameterTypes.length == ctr.getParameterTypes.length) + new ConstructorBeanCreator(TypeUtil.getRawClass(ofType), ctr, resolvedNames, parameterTypes) + else null + } + } + + def createCreator(ofType: Type, method: Method, resolvedNames: Array[String], genson: Genson): BeanCreator = { + ScalaReflectionApiLock.synchronized { + val parameterTypes = matchingMethod(method).paramss + .flatten + .flatMap(p => expandToJavaType(p.typeSignature, ofType)) + .toArray + if (parameterTypes.length == method.getParameterTypes.length) + new MethodBeanCreator(method, resolvedNames, parameterTypes) + else null + } + } + + def createMutator(name: String, field: Field, ofType: Type, genson: Genson): PropertyMutator = { + fieldType(field, ofType).map(new FieldMutator(name, field, _, field.getDeclaringClass)).getOrElse(null) + } + + def createMutator(name: String, method: Method, ofType: Type, genson: Genson): PropertyMutator = { + ScalaReflectionApiLock.synchronized { + val sType = expandToJavaType(matchingMethod(method).paramss.flatten.head, ofType) + sType.map(new MethodMutator(name, method, _, method.getDeclaringClass)).getOrElse(null) + } + } + + private def matchingMethod(method: Method): u.MethodSymbol = { + ScalaReflectionApiLock.synchronized { + val matchingMethods = mirror.classSymbol(method.getDeclaringClass) + .selfType + .declaration(u.newTermName(method.getName)) + .asTerm + .alternatives + .filter(p => p.asMethod.paramss.flatten.length == method.getParameterTypes.length) + + if (matchingMethods.length != 1) + throw new UnsupportedOperationException("Failed to match single accessor, please report this issue.") + + matchingMethods.head.asMethod + } + } + + private def fieldType(field: Field, ofType: Type): Option[Type] = { + ScalaReflectionApiLock.synchronized { + val term = u.newTermName(field.getName) + val typeSig = mirror.classSymbol(field.getDeclaringClass).selfType.member(term).typeSignature + expandToJavaType(typeSig, ofType) + } + } + + private def expandToJavaType(scalaType: Any, rootType: Type): Option[Type] = { + if (scalaType.isInstanceOf[u.NullaryMethodTypeApi]) { + expandToJavaType(scalaType.asInstanceOf[u.NullaryMethodTypeApi].resultType, rootType) + } else if (scalaType.isInstanceOf[u.TypeRefApi]) { + expandTypeRef(scalaType.asInstanceOf[u.TypeRefApi], rootType) + } else if (scalaType.isInstanceOf[u.ClassSymbolApi]) { + expandClassSymbol(scalaType.asInstanceOf[u.ClassSymbol], rootType) + } else { + None + } + } + + private def expandClassSymbol(t: u.ClassSymbol, rootType: Type): Option[Type] = { + if (t.isAliasType) + expandToJavaType(t.toType.asInstanceOf[scala.reflect.internal.Types#Type].dealias, rootType) + else if (t.isAbstractType) { + // TODO handle bounded types or fallback to type resolution implemented in TypeUtil + val stv = new ScalaTypeVariable(t.name.decoded, Array(classOf[Object]), TypeUtil.getRawClass(rootType)) + Option(TypeUtil.expandType(stv, rootType)) + } else { + val name = t.fullName + if (name != "scala.Any" && name != "scala.AnyRef" && name != "scala.AnyVal") + Option(mirror.runtimeClass(t)) + else Option(classOf[Object]) + } + } + + private def expandTypeRef(t: u.TypeRefApi, rootType: Type): Option[Type] = { + val isArray = t.sym.fullName == "scala.Array" + + if (t.args.nonEmpty) { + val args = t.args.flatMap(p => expandToJavaType(p, rootType)).toArray + + if (args.length != t.args.length) { + None + } else { + val expandedArgs = args.map { expandedType => + val rawType = TypeUtil.getRawClass(expandedType) + if (!isArray && rawType.isPrimitive) TypeUtil.wrap(rawType) + else expandedType + } + + if (t.sym.fullName == "scala.Array") { + if (args.length > 1) { + // should not happen + throw new UnsupportedOperationException() + } + + Option(new ScalaGenericArrayType(args.head)) + } else expandToJavaType(t.sym, rootType).map(new ScalaParameterizedType(None, _, expandedArgs)) + } + } else expandToJavaType(t.sym, rootType) + } +} diff --git a/genson-scala/src/main/scala/com/owlike/genson/ScalaBundle.scala b/genson-scala/src/main/scala/com/owlike/genson/ScalaBundle.scala index 65450ad2..2b33821a 100644 --- a/genson-scala/src/main/scala/com/owlike/genson/ScalaBundle.scala +++ b/genson-scala/src/main/scala/com/owlike/genson/ScalaBundle.scala @@ -18,6 +18,7 @@ class ScalaBundle extends GensonBundle { .withConverterFactory(ScalaUntypedConverterFactory) .withConverterFactory(new TupleConverterFactory()) .withConverterFactory(new OptionConverterFactory()) + .withBeanPropertyFactory(new ScalaBeanPropertyFactory()) } def useOnlyConstructorFields(enable: Boolean): ScalaBundle = { diff --git a/genson-scala/src/main/scala/com/owlike/genson/ScalaGenson.scala b/genson-scala/src/main/scala/com/owlike/genson/ScalaGenson.scala index 0e1c10c1..618b8dac 100644 --- a/genson-scala/src/main/scala/com/owlike/genson/ScalaGenson.scala +++ b/genson-scala/src/main/scala/com/owlike/genson/ScalaGenson.scala @@ -1,6 +1,6 @@ package com.owlike.genson -import java.lang.reflect.{Type => JType, GenericArrayType, ParameterizedType} +import java.lang.reflect.{Type => JType, GenericDeclaration, TypeVariable, GenericArrayType, ParameterizedType} import java.io.{Reader => JReader, _} import java.net.URL @@ -39,7 +39,7 @@ class ScalaGenson(val genson: Genson) extends AnyVal { genson.deserialize(GenericType.of(toJavaType), reader, new Context(genson)).asInstanceOf[T] } - private def toJavaType(implicit m: Manifest[_]): JType = { + private[genson] def toJavaType(implicit m: Manifest[_]): JType = { if (m.runtimeClass.isArray) { m.typeArguments .headOption @@ -54,11 +54,21 @@ class ScalaGenson(val genson: Genson) extends AnyVal { } } -private class ScalaGenericArrayType(componentType: JType) extends GenericArrayType { +private[genson] class ScalaTypeVariable(name: String, bounds: Array[JType], decl: Class[_]) + extends TypeVariable[Class[_]] { + + def getBounds: Array[JType] = bounds + + def getGenericDeclaration: Class[_] = decl + + def getName: String = name +} + +private[genson] class ScalaGenericArrayType(componentType: JType) extends GenericArrayType { def getGenericComponentType: JType = componentType } -private class ScalaParameterizedType(val ownerType: Option[JType], val rawType: JType, val typeArgs: Array[JType]) +private[genson] class ScalaParameterizedType(val ownerType: Option[JType], val rawType: JType, val typeArgs: Array[JType]) extends ParameterizedType { def getOwnerType: JType = ownerType.getOrElse(null) @@ -67,14 +77,14 @@ private class ScalaParameterizedType(val ownerType: Option[JType], val rawType: def getActualTypeArguments: Array[JType] = typeArgs - def canEqual(other: Any): Boolean = other.isInstanceOf[ScalaParameterizedType] + def canEqual(other: Any): Boolean = other.isInstanceOf[ParameterizedType] override def equals(other: Any): Boolean = other match { - case that: ScalaParameterizedType => - (that canEqual this) && - ownerType == that.ownerType && - rawType == that.rawType && - typeArgs == that.typeArgs + case that: ParameterizedType => + (this canEqual that) && + ownerType == that.getOwnerType && + rawType == that.getRawType && + typeArgs == that.getActualTypeArguments case _ => false } diff --git a/genson-scala/src/test/scala/com/owlike/genson/Model.scala b/genson-scala/src/test/scala/com/owlike/genson/Model.scala index 8f0e0113..2ca7cef3 100644 --- a/genson-scala/src/test/scala/com/owlike/genson/Model.scala +++ b/genson-scala/src/test/scala/com/owlike/genson/Model.scala @@ -1,5 +1,8 @@ package com.owlike.genson +import scala.beans.BeanProperty +import scala.language.existentials + case class BasicPoso(val aString: String, val aInt: Int, val aBoolean: Boolean) { val shouldBeIgnored: Int = 10 var other: String = _ @@ -12,3 +15,22 @@ case class PosoWithOption(val optPoso: Option[BasicPoso], val optInt: Option[Int case class PosoWithMultipleCtrs(val aString: String, val aInt: Int) { def this(aInt2: Int) = this("", aInt2) } + +case class PosoWithArrayOfPrimitives(val array: Array[Int]) + +case class PosoWithNestedGenerics(val genValue: GenericHolder[GenericHolder[Int]]) + +case class GenericHolder[T](val v: T) + +case class PosoWithBoundedGenericHolder(val arrayHolder: GenericHolder[_ <: BaseClass]) + +case class BaseClass(val aInt: Int) + +class ClassWithGetterAndFiels(@BeanProperty val aStr: String, @BeanProperty val anArray: Array[Option[Int]]) { + var aIntWithPrivateName: Option[Int] = None + + def getAInt() = aIntWithPrivateName + def setAInt(aInt: Option[Int]) = this.aIntWithPrivateName = aInt +} + +case class CaseClassWithAny(any: Any, anyRef: AnyRef, anyVal: AnyVal) diff --git a/genson-scala/src/test/scala/com/owlike/genson/ScalaPojoRoundTripTest.scala b/genson-scala/src/test/scala/com/owlike/genson/ScalaPojoRoundTripTest.scala index eaf2c6cc..9dbe3aa2 100644 --- a/genson-scala/src/test/scala/com/owlike/genson/ScalaPojoRoundTripTest.scala +++ b/genson-scala/src/test/scala/com/owlike/genson/ScalaPojoRoundTripTest.scala @@ -5,6 +5,49 @@ import defaultGenson._ class ScalaPojoRoundTripTest extends FunSuite with Matchers { + test("should serialize correctly case class containing a property of type Any") { + val expected = "{\"any\":1,\"anyRef\":{\"aInt\":2},\"anyVal\":3}" + toJson(CaseClassWithAny(1, BaseClass(2), 3)) should be (expected) + } + + test("deser of tuple with Optional primitive") { + val expected = (1, Option(2), "foo") + fromJson[(Int, Option[Int], String)](toJson(expected)) should be (expected) + } + + test("round trip of ClassWithGetterAndFiels should work with generics of primitive using get/set") { + val expected = new ClassWithGetterAndFiels("foo", Array(Option(1), None, Option(2))) + expected.setAInt(Option(2)) + + val actual = fromJson[ClassWithGetterAndFiels](toJson(expected)) + actual.aStr should be (expected.aStr) + actual.anArray.toSeq should be (expected.anArray.toSeq) + actual.getAInt() should be (expected.getAInt()) + } + + test("deser poso using upper type bound") { + val json = toJson(PosoWithBoundedGenericHolder(GenericHolder(BaseClass(1)))) + fromJson[PosoWithBoundedGenericHolder](json).arrayHolder.v.aInt == 1 should be (true) + } + + test("deser poso with nested generics") { + val expected = PosoWithNestedGenerics(GenericHolder(GenericHolder(1))) + expected.genValue.v.v.getClass should be (classOf[Int]) + fromJson[PosoWithNestedGenerics](toJson(expected)) should be (expected) + } + + test("deser poso with array of primitive int") { + val expected = PosoWithArrayOfPrimitives(Array(1, 2, 3)) + fromJson[PosoWithArrayOfPrimitives](toJson(expected)).array.toSeq should be (expected.array.toSeq) + } + + test("deser generic primitive scala type") { + val expected = PosoWithOption(Some(BasicPoso("foo", 3, true)), Some(2)) + + val v = fromJson[PosoWithOption](toJson(expected)).optInt.get + v should be (2) + } + test("case class with primitive val attributes") { val expected = BasicPoso("foo", 3, true) expected.other = "bar" diff --git a/genson/src/main/java/com/owlike/genson/reflect/TypeUtil.java b/genson/src/main/java/com/owlike/genson/reflect/TypeUtil.java index fce7ddd3..c2b133bf 100644 --- a/genson/src/main/java/com/owlike/genson/reflect/TypeUtil.java +++ b/genson/src/main/java/com/owlike/genson/reflect/TypeUtil.java @@ -404,7 +404,6 @@ private final static Type[] getTypes(Type type) { * ExpandedType must implement hashcode and equals using only the original type * The root class is not significant here as a type expanded in different root classes can yield to the same expanded type. * Very important, hashcode and equals must also be implemented in subclasses and also use those from ExpandedType. - * Was done previously but incorrectly and then removed, my fault :/ * * http://code.google.com/p/genson/issues/detail?id=4 */