Skip to content

Commit

Permalink
fixing erased primitive types in generics for scala, a lot of ugly co…
Browse files Browse the repository at this point in the history
…de but seems to work...
  • Loading branch information
EugenCepoi committed Nov 11, 2014
1 parent 61b1534 commit 4f0d14d
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class ScalaBundle extends GensonBundle {
.withConverterFactory(ScalaUntypedConverterFactory)
.withConverterFactory(new TupleConverterFactory())
.withConverterFactory(new OptionConverterFactory())
.withBeanPropertyFactory(new ScalaBeanPropertyFactory())
}

def useOnlyConstructorFields(enable: Boolean): ScalaBundle = {
Expand Down
30 changes: 20 additions & 10 deletions genson-scala/src/main/scala/com/owlike/genson/ScalaGenson.scala
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
}

Expand Down
22 changes: 22 additions & 0 deletions genson-scala/src/test/scala/com/owlike/genson/Model.scala
Original file line number Diff line number Diff line change
@@ -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 = _
Expand All @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down

0 comments on commit 4f0d14d

Please sign in to comment.