Question

Is it possible to get ClassTag information from a Java Class instance obtained via reflection?

Here's the situation. I have a Scala case class that looks like this:

case class Relation[M : ClassTag](id: UUID, 
                                  model: Option[M] = None)

And it is used like this (although with many more classes related to each other):

case class Organization(name: String)

case class Person(firstName: String, 
                  lastName: String,
                  organization: Relation[Organization])

What I'm trying to do is programmatically build up a tree of these relations using something that looks like this:

private def generateFieldMap(clazz: Class[_]): Map[String, Class[_]] = {
    clazz.getDeclaredFields.foldLeft(Map.empty[String, Class[_]])((map, field) => {
        map + (field.getName -> field.getType)
    })
}

private def getRelationModelClass[M : ClassTag](relationClass: Class[_ <: Relation[M]]): Class[_] = {
    classTag[M].runtimeClass
}

def treeOf[M: ClassTag](relations: List[String]): Map[String, Any] = {
    val normalizedRelations = ModelHelper.normalize(relations)
    val initialFieldMap = Map("" -> generateFieldMap(classTag[M].runtimeClass))
    val relationFieldMap = relations.foldLeft(initialFieldMap)((map, relation) => {
        val parts = relation.split('.')
        val parentRelation = parts.dropRight(1).mkString(".")
        val relationClass = map(parentRelation)(parts.last)
        val relationModelClass = relationClass match {
            case clazz: Class[_ <: Relation[_]] => getRelationModelClass(clazz)
            case _ => throw ProcessStreetException("cannot follow non-relation: " + relation)
        }
        val fieldMap = generateFieldMap(relationModelClass)
        map + (relation -> fieldMap)
    })
    relationFieldMap
}

val relations = List("organization")
val tree = treeOf[Person](relations)

This won't compile. I get this error:

[error] Foo.scala:148: not found: type _$12
[error]                 case clazz: Class[_ <: Relation[_]] => getRelationModelClass(clazz)
[error]                                   ^
[error] one error found
[error] (compile:compile) Compilation failed

Basically, what I'd like to do is be able to access the ClassTag information when all I have is a Java Class. Is this possible?

Was it helpful?

Solution

Yes, it is absolutely possible and very easy:

val clazz = classOf[String]
val ct = ClassTag(clazz)  // just use ClassTag.apply() method

In your example you'd want to call getRelationModelClass method like this:

getRelationModelClass(clazz)(ClassTag(clazz))

This is possible because [T: ClassTag] syntax implicitly creates second parameters list like (implicit ct: ClassTag[T]). Usually it is filled by the compiler, but nothing prevents you from using it explicitly.

You also don't really need to pass the class AND class tag for this clazz at the same time to the method. You're not even using explicit class object in its body. Just pass the class tag, it will be enough.

OTHER TIPS

I ended up accomplishing my goal using TypeTags and the Scala reflection API. Here are the changes necessary.

First, change the Relation class to use a TypeTag.

case class Relation[M : TypeTag](id: UUID, 
                                 model: Option[M] = None)

Then change the rest of the code to use the Scala reflection API:

private def generateFieldMap(tpe: Type): Map[String, Type] =
    tpe.members.filter(_.asTerm.isVal).foldLeft(Map.empty[String, Type])((map, field) => {
        map + (member.name.toString.trim -> member.typeSignature)
    })

private def getRelationModelType(tpe: Type): Type = 
    tpe match { case TypeRef(_, _, args) => args.head }

def treeOf[M: TypeTag](relations: List[String]): Map[String, Any] = {
    val normalizedRelations = ModelHelper.normalize(relations)
    val initialFieldMap = Map("" -> generateFieldMap(typeTag[T].tpe))
    val relationFieldMap = relations.foldLeft(initialFieldMap)((map, relation) => {
        val parts = relation.split('.')
        val parentRelation = parts.dropRight(1).mkString(".")
        val relationType = map(parentRelation)(parts.last)
        val relationModelType = getRelationModelType(relationType)
        val fieldMap = generateFieldMap(relationModelType)
        map + (relation -> fieldMap)
    })
    relationFieldMap
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top