Scala : Transforming a List into a nested Map - java

I have an ConfigEntry class defined as
case class ConfigEntry(
key: String,
value: String
)
and a list:
val list: List[ConfigEntry] = List(
ConfigEntry("general.first", "general first value"),
ConfigEntry("general.second", "general second value"),
ConfigEntry("custom.first", "custom first value"),
ConfigEntry("custom.second", "custom second value")
)
Given a list of ConfigEntry, I want a map from property -> map of entries that satisfy that property
As an example, if I have
def getConfig: Map[String, Map[String, String]] = {
def getKey(key: String, index: Int): String = key.split("\\.")(index)
list.map { config =>
getKey(config.key, 0) -> Map(getKey(config.key, 1) -> config.value)
}.toMap
}
I get result
res0: Map[String,Map[String,String]] =
Map(
"general" ->
Map("second" -> "general second value"),
"custom" ->
Map("second" -> "custom second value")
)
and it should be
res0: Map[String,Map[String,String]] =
Map(
"general" ->
Map(
"first" -> "general first value",
"second" -> "general second value"
),
"custom" ->
Map(
"first" -> "custom first value",
"second" -> "custom second value"
)
)
The first record from the list is missing. It's probably through .toMap
How can I do this?
Thank you for any help given

You can do something like this:
final case class ConfigEntry(
key: String,
value: String
)
type Config = Map[String, Map[String, String]]
def getConfig(data: List[ConfigEntry]): Config =
data
.view
.map(e => e.key.split('.').toList -> e.value)
.collect {
case (k1 :: k2 :: Nil, v) => k1 -> (k2 -> v)
}.groupMap(_._1)(_._2)
.view
.mapValues(_.toMap)
.toMap
Or something like this:
def getConfig(data: List[ConfigEntry]): Config = {
#annotation.tailrec
def loop(remaining: List[ConfigEntry], acc: Config): Config =
remaining match {
case ConfigEntry(key, value) :: xs =>
val newAcc = key.split('.').toList match {
case k1 :: k2 :: Nil =>
acc.updatedWith(k1) {
case Some(map) =>
val newMap = map.updatedWith(k2) {
case Some(v) =>
println(s"Overwriting previous value ${v} for the key: ${key}")
// Just overwrite the previous value.
Some(value)
case None =>
Some(value)
}
Some(newMap)
case None =>
Some(Map(k2 -> value))
}
case _ =>
println(s"Bad key: ${key}")
// Just skip this key.
acc
}
loop(remaining = xs, newAcc)
case Nil =>
acc
}
loop(remaining = data, acc = Map.empty)
}
I leave the handling of errors like duplicated keys or bad keys to the reader.
BTW, since this is a config, have you considered using a Config library?

Your map will only produce a 1 to 1 result. To do what you want you will need an accumulator (existing map) to do this.
Working with your existing code, if you're especially tied to how you're parsing your primary and secondary keys via getKey you can apply foldLeft to your list instead, with an empty map as an initial value.
list.foldLeft(Map.empty[String, Map[String, String]]) { (configs, configEntry) =>
val primaryKey = getKey(configEntry.key, 0)
val secondaryKey = getKey(configEntry.key, 1)
configs.get(primaryKey) match {
case None =>
configs.updated(primaryKey, Map(secondaryKey -> configEntry.value))
case Some(configMap) =>
configs.updated(primaryKey, configMap.updated(secondaryKey, configEntry.value))
}
}

Simply:
list.map { ce =>
val Array(l, r) = ce.key.split("\\.")
l -> (r -> ce.value)
} // List[(String, (String, String))]
.groupBy { case (k, _) => k } // Map[String, List[(String, (String, String))]]
.view.mapValues(_.map { case (_, v) => v }.toMap) // MapView[String, List[(String, String)]]
.toMap // Map[String, Map[String, String]]

Related

How to get value of KProperty1

I have an object
I try to get access to field "english"
val englishSentence = dbField::class.declaredMemberProperties.filter { it.name == "english" }[0]
But when I
model.addAttribute("sentence", englishSentence)
I get val com.cyrillihotin.grammartrainer.entity.Sentence.english: kotlin.String
while I expect bla
You can use the call function on a KProperty to get its value from the object.
val dbField = Sentence(1, "bla-eng", "bla-rus")
val value = dbField::class.declaredMemberProperties.find { it.name == "english" }!!.call(dbField)
println(value)
Output: bla-eng
Remember that data type of value is Any here. You need to cast it manually to the desired data type.
If you want to list all the properties with their values, you can do this:
dbField::class.declaredMemberProperties.forEach {
println("${it.name} -> ${it.call(dbField)}")
}
Output:
english -> bla-eng
id -> 1
russian -> bla-rus
Do you mean this?
data class Sentence(val id:Int, val english:String, val russian:String)
val dbField = Sentence(1, "blaEng", "blaRus")
val englishProp = dbField::class.declaredMemberProperties.first { it.name == "english" }as KProperty1<Sentence, String>
println(englishProp.get(dbField))
It prints blaEng

Flattening a 3 level nested JSON string in java

The requirement is to create a generic flattening utility for an input JSON object to a flattened JSON object.
The sample JSON looks like the below
{
"Source": "source-1",
"Rows": [
{
"Keys": {
"device-id": "BC04-EBH-N3K-01",
"interface-name": "TenGigE0/0/0/39",
"node-name": "0/0/CPU0"
},
"Timestamp": 1567621527656,
"inner": {
"donm": {
"id": "0062",
"mol": {
"rem": 30,
"len": 11,
"org": {
"ldp": [
{
"t": 486,
"o": 322
},
{
"t": 487,
"o": 32,
"twss": 1,
"tlv": "00:01"
}
]
},
"chlen": 14,
"poe": 5,
"combs": 10,
"chaype": 4,
"rek": 0,
"rem-um": 67
},
"detail": {
"enas": "B,R",
"systes": "B,R",
"timng": 91,
"syn": "C",
"met-type": 0,
"neses": {
"lldEDIT": [
{
"ium": 830,
"m": 1,
"ass": {
"ape": "ipv4",
"ipvs": "94"
}
}
]
},
"pess": "0008",
"por]d": 0,
"pon": "BCtive",
"sysme": "BC1"
},
"reme": "Bu1",
"hean": 0,
"porl": "Et1"
}
}
}
],
"Tey": {
"epath": "Cgetail",
"sustr": "MX",
"coime": 1567621527653,
"msp": 1567621527653,
"come": 1567621527660,
"nor": "BC5",
"cid": 14789654
}
}
I have been trying to flatten it to 3 levels and came up with the below utility. But, The things are getting complicated when I have to deal with Arrays and values of type String, long, Timestamp, etc. Also, I am unable to understand how the nested keys can be maintained for uniqueness.
public static Map<String,Object> flattenJson(JsonNode input){
Map<String,Object> finalMap = new HashMap<>();
ObjectMapper datamapper = new ObjectMapper();
Map<String,Object> topLevelJsonMap = datamapper.convertValue(input,Map.class);
Set<String> topLevelKeys = topLevelJsonMap.keySet();
for(String topLevelKey : topLevelKeys){
System.out.println("Key :::: "+topLevelKey);
Object topLevelData = topLevelJsonMap.get(topLevelKey);
System.out.println("value :::: "+topLevelData.toString());
if(topLevelData instanceof ArrayNode){
ArrayNode arrayOfData = (ArrayNode) topLevelData;
for(JsonNode dataNode : arrayOfData){
flattenJson(input);
}
} else if(topLevelData instanceof JsonNode){
Map<String,Object> innerLevelJsonMap = datamapper.convertValue(topLevelData,Map.class);
Set<String> innerLevelKeys = innerLevelJsonMap.keySet();
for(String innerLevelKey : innerLevelKeys){
System.out.println("inner key :::: "+innerLevelKey);
flattenJson((JsonNode) innerLevelJsonMap.get(innerLevelKey));
}
}else {
finalMap.put(topLevelKey,topLevelData);
}
}
return finalMap;
}
Any help is greatly appreciated.
You can take a look on json-flattener.
BTW, I am the author of this lib.
To avoid conflicts with key names you can use JSON Pointer specification to create them. It is also supported by Jackson library, so you can use them later to traverse the JsonNode node.
Simple implementation could look like below:
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.File;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
public class JsonApp {
public static void main(String[] args) throws Exception {
File jsonFile = new File("./test.json");
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(jsonFile);
Map<String, JsonNode> map = new JsonFlattener(root).flatten();
System.out.println("Use key-value pairs:");
map.forEach(
(k, v) -> {
System.out.println(k + " => " + v);
});
System.out.println();
System.out.println("Use pointers:");
map.forEach(
(k, v) -> {
System.out.println(k + " => " + root.at(k));
});
}
}
class JsonFlattener {
private final Map<String, JsonNode> json = new LinkedHashMap<>(64);
private final JsonNode root;
JsonFlattener(JsonNode node) {
this.root = Objects.requireNonNull(node);
}
public Map<String, JsonNode> flatten() {
process(root, "");
return json;
}
private void process(JsonNode node, String prefix) {
if (node.isObject()) {
ObjectNode object = (ObjectNode) node;
object
.fields()
.forEachRemaining(
entry -> {
process(entry.getValue(), prefix + "/" + entry.getKey());
});
} else if (node.isArray()) {
ArrayNode array = (ArrayNode) node;
AtomicInteger counter = new AtomicInteger();
array
.elements()
.forEachRemaining(
item -> {
process(item, prefix + "/" + counter.getAndIncrement());
});
} else {
json.put(prefix, node);
}
}
}
Above code prints:
Use key-value pairs:
/Source => "source-1"
/Rows/0/Keys/device-id => "BC04-EBH-N3K-01"
/Rows/0/Keys/interface-name => "TenGigE0/0/0/39"
/Rows/0/Keys/node-name => "0/0/CPU0"
/Rows/0/Timestamp => 1567621527656
/Rows/0/inner/donm/id => "0062"
/Rows/0/inner/donm/mol/rem => 30
/Rows/0/inner/donm/mol/len => 11
/Rows/0/inner/donm/mol/org/ldp/0/t => 486
/Rows/0/inner/donm/mol/org/ldp/0/o => 322
/Rows/0/inner/donm/mol/org/ldp/1/t => 487
/Rows/0/inner/donm/mol/org/ldp/1/o => 32
/Rows/0/inner/donm/mol/org/ldp/1/twss => 1
/Rows/0/inner/donm/mol/org/ldp/1/tlv => "00:01"
/Rows/0/inner/donm/mol/chlen => 14
/Rows/0/inner/donm/mol/poe => 5
/Rows/0/inner/donm/mol/combs => 10
/Rows/0/inner/donm/mol/chaype => 4
/Rows/0/inner/donm/mol/rek => 0
/Rows/0/inner/donm/mol/rem-um => 67
/Rows/0/inner/donm/detail/enas => "B,R"
/Rows/0/inner/donm/detail/systes => "B,R"
/Rows/0/inner/donm/detail/timng => 91
/Rows/0/inner/donm/detail/syn => "C"
/Rows/0/inner/donm/detail/met-type => 0
/Rows/0/inner/donm/detail/neses/lldEDIT/0/ium => 830
/Rows/0/inner/donm/detail/neses/lldEDIT/0/m => 1
/Rows/0/inner/donm/detail/neses/lldEDIT/0/ass/ape => "ipv4"
/Rows/0/inner/donm/detail/neses/lldEDIT/0/ass/ipvs => "94"
/Rows/0/inner/donm/detail/pess => "0008"
/Rows/0/inner/donm/detail/por]d => 0
/Rows/0/inner/donm/detail/pon => "BCtive"
/Rows/0/inner/donm/detail/sysme => "BC1"
/Rows/0/inner/donm/reme => "Bu1"
/Rows/0/inner/donm/hean => 0
/Rows/0/inner/donm/porl => "Et1"
/Tey/epath => "Cgetail"
/Tey/sustr => "MX"
/Tey/coime => 1567621527653
/Tey/msp => 1567621527653
/Tey/come => 1567621527660
/Tey/nor => "BC5"
/Tey/cid => 14789654
Use pointers:
/Source => "source-1"
/Rows/0/Keys/device-id => "BC04-EBH-N3K-01"
/Rows/0/Keys/interface-name => "TenGigE0/0/0/39"
/Rows/0/Keys/node-name => "0/0/CPU0"
/Rows/0/Timestamp => 1567621527656
/Rows/0/inner/donm/id => "0062"
/Rows/0/inner/donm/mol/rem => 30
/Rows/0/inner/donm/mol/len => 11
/Rows/0/inner/donm/mol/org/ldp/0/t => 486
/Rows/0/inner/donm/mol/org/ldp/0/o => 322
/Rows/0/inner/donm/mol/org/ldp/1/t => 487
/Rows/0/inner/donm/mol/org/ldp/1/o => 32
/Rows/0/inner/donm/mol/org/ldp/1/twss => 1
/Rows/0/inner/donm/mol/org/ldp/1/tlv => "00:01"
/Rows/0/inner/donm/mol/chlen => 14
/Rows/0/inner/donm/mol/poe => 5
/Rows/0/inner/donm/mol/combs => 10
/Rows/0/inner/donm/mol/chaype => 4
/Rows/0/inner/donm/mol/rek => 0
/Rows/0/inner/donm/mol/rem-um => 67
/Rows/0/inner/donm/detail/enas => "B,R"
/Rows/0/inner/donm/detail/systes => "B,R"
/Rows/0/inner/donm/detail/timng => 91
/Rows/0/inner/donm/detail/syn => "C"
/Rows/0/inner/donm/detail/met-type => 0
/Rows/0/inner/donm/detail/neses/lldEDIT/0/ium => 830
/Rows/0/inner/donm/detail/neses/lldEDIT/0/m => 1
/Rows/0/inner/donm/detail/neses/lldEDIT/0/ass/ape => "ipv4"
/Rows/0/inner/donm/detail/neses/lldEDIT/0/ass/ipvs => "94"
/Rows/0/inner/donm/detail/pess => "0008"
/Rows/0/inner/donm/detail/por]d => 0
/Rows/0/inner/donm/detail/pon => "BCtive"
/Rows/0/inner/donm/detail/sysme => "BC1"
/Rows/0/inner/donm/reme => "Bu1"
/Rows/0/inner/donm/hean => 0
/Rows/0/inner/donm/porl => "Et1"
/Tey/epath => "Cgetail"
/Tey/sustr => "MX"
/Tey/coime => 1567621527653
/Tey/msp => 1567621527653
/Tey/come => 1567621527660
/Tey/nor => "BC5"
/Tey/cid => 14789654
Try this code:
public static void flattenJson(JsonNode node, String parent, Map<String, ValueNode> map) {
if (node instanceof ValueNode) {
map.put(parent, (ValueNode)node);
} else {
String prefix = parent == null ? "" : parent + ".";
if (node instanceof ArrayNode) {
ArrayNode arrayNode = (ArrayNode)node;
for(int i = 0; i < arrayNode.size(); i++) {
flattenJson(arrayNode.get(i), prefix + i, map);
}
} else if (node instanceof ObjectNode) {
ObjectNode objectNode = (ObjectNode) node;
for (Iterator<Map.Entry<String, JsonNode>> it = objectNode.fields(); it.hasNext(); ) {
Map.Entry<String, JsonNode> field = it.next();
flattenJson(field.getValue(), prefix + field.getKey(), map);
}
} else {
throw new RuntimeException("unknown json node");
}
}
}
public static Map<String, ValueNode> flattenJson(JsonNode input) {
Map<String, ValueNode> map = new LinkedHashMap<>();
flattenJson(input, null, map);
return map;
}
Then you can call
ObjectMapper om = new ObjectMapper();
JsonNode jsonNode = om.readTree(json);
Map<String, ValueNode> m = flattenJson(jsonNode);
for (Map.Entry<String, ValueNode> kv : m.entrySet()) {
System.out.println(kv.getKey() + "=" + kv.getValue().asText());
}
Output:
Source=source-1
Rows.0.Keys.device-id=BC04-EBH-N3K-01
Rows.0.Keys.interface-name=TenGigE0/0/0/39
Rows.0.Keys.node-name=0/0/CPU0
Rows.0.Timestamp=1567621527656
Rows.0.inner.donm.id=0062
Rows.0.inner.donm.mol.rem=30
Rows.0.inner.donm.mol.len=11
Rows.0.inner.donm.mol.org.ldp.0.t=486
Rows.0.inner.donm.mol.org.ldp.0.o=322
Rows.0.inner.donm.mol.org.ldp.1.t=487
Rows.0.inner.donm.mol.org.ldp.1.o=32
Rows.0.inner.donm.mol.org.ldp.1.twss=1
Rows.0.inner.donm.mol.org.ldp.1.tlv=00:01
Rows.0.inner.donm.mol.chlen=14
Rows.0.inner.donm.mol.poe=5
Rows.0.inner.donm.mol.combs=10
Rows.0.inner.donm.mol.chaype=4
Rows.0.inner.donm.mol.rek=0
Rows.0.inner.donm.mol.rem-um=67
Rows.0.inner.donm.detail.enas=B,R
Rows.0.inner.donm.detail.systes=B,R
Rows.0.inner.donm.detail.timng=91
Rows.0.inner.donm.detail.syn=C
Rows.0.inner.donm.detail.met-type=0
Rows.0.inner.donm.detail.neses.lldEDIT.0.ium=830
Rows.0.inner.donm.detail.neses.lldEDIT.0.m=1
Rows.0.inner.donm.detail.neses.lldEDIT.0.ass.ape=ipv4
Rows.0.inner.donm.detail.neses.lldEDIT.0.ass.ipvs=94
Rows.0.inner.donm.detail.pess=0008
Rows.0.inner.donm.detail.por]d=0
Rows.0.inner.donm.detail.pon=BCtive
Rows.0.inner.donm.detail.sysme=BC1
Rows.0.inner.donm.reme=Bu1
Rows.0.inner.donm.hean=0
Rows.0.inner.donm.porl=Et1
Tey.epath=Cgetail
Tey.sustr=MX
Tey.coime=1567621527653
Tey.msp=1567621527653
Tey.come=1567621527660
Tey.nor=BC5
Tey.cid=14789654
Just create namespace of the keys by appending each level key into the key from higher level.
In other words your flattened JSON's keys would be:
{
"L1key::L2key": "L2val",
"L1key": "L1val",
"L1key::L2key::L3key": "L3val"
}
This way you guarantee uniqueness but also you can create the original json from this as well. Lastly be sure that the level separator (here ::) will not be present in your key.
HTH

Spark Accumulator

I am new to accumulators in Spark . I have created an accumulator which gathers the information of sum and count of all columns in a dataframe into a Map.
Which is not functioning as expected , so I have a few doubts.
When I run this class( pasted below) in local mode , I can see that the accumulators getting updated but the final value is still empty.For debug purposes, I added a print Statement in add() .
Q1) Why is the final accumulable not being updated when the accumulator is being added ?
For reference , I studied the CollectionsAccumulator where they have made use of SynchronizedList from Java Collections.
Q2) Does it need to be a synchronized/concurrent collection for an accumulator to update ?
Q3) Which collection will be best suited for such purpose ?
I have attached my execution flow along with spark ui snapshot for analysis.
Thanks.
EXECUTION:
INPUT DATAFRAME -
+-------+-------+
|Column1|Column2|
+-------+-------+
|1 |2 |
|3 |4 |
+-------+-------+
OUTPUT -
Add - Map(Column1 -> Map(sum -> 1, count -> 1), Column2 -> Map(sum -> 2, count -> 1))
Add - Map(Column1 -> Map(sum -> 4, count -> 2), Column2 -> Map(sum -> 6, count -> 2))
TestRowAccumulator(id: 1, name: Some(Test Accumulator for Sum&Count), value: Map())
SPARK UI SNAPSHOT -
CLASS :
class TestRowAccumulator extends AccumulatorV2[Row,Map[String,Map[String,Int]]]{
private var colMetrics: Map[String, Map[String, Int]] = Map[String , Map[String , Int]]()
override def isZero: Boolean = this.colMetrics.isEmpty
override def copy(): AccumulatorV2[Row, Map[String,Map[String,Int]]] = {
val racc = new TestRowAccumulator
racc.colMetrics = colMetrics
racc
}
override def reset(): Unit = {
colMetrics = Map[String,Map[String,Int]]()
}
override def add(v: Row): Unit = {
v.schema.foreach(field => {
val name: String = field.name
val value: Int = v.getAs[Int](name)
if(!colMetrics.contains(name))
{
colMetrics = colMetrics ++ Map(name -> Map("sum" -> value , "count" -> 1 ))
}else
{
val metric = colMetrics(name)
val sum = metric("sum") + value
val count = metric("count") + 1
colMetrics = colMetrics ++ Map(name -> Map("sum" -> sum , "count" -> count))
}
})
}
override def merge(other: AccumulatorV2[Row, Map[String,Map[String,Int]]]): Unit = {
other match {
case t:TestRowAccumulator => {
colMetrics.map(col => {
val map2: Map[String, Int] = t.colMetrics.getOrElse(col._1 , Map())
val map1: Map[String, Int] = col._2
map1 ++ map2.map{ case (k,v) => k -> (v + map1.getOrElse(k,0)) }
} )
}
case _ => throw new UnsupportedOperationException(s"Cannot merge ${this.getClass.getName} with ${other.getClass.getName}")
}
}
override def value: Map[String, Map[String, Int]] = {
colMetrics
}
}
After a bit of debug , I found that merge function is being called .
It had erroneous code so the accumulable value was Map()
EXECUTION FlOW OF ACCUMULATOR (LOCAL MODE) :
ADD
ADD
MERGE
Once I corrected the merge function , accumulator worked as expected

create a map from list in Scala

I need to create a HashMap of directory-to-file in scala while I list all files in the directory. How can I achieve this in scala?
val directoryToFile = awsClient.listFiles(uploadPath).collect {
case path if !path.endsWith("/") => {
path match {
// do some regex matching to get directory & file names
case regex(dir, date) => {
// NEED TO CREATE A HASH MAP OF dir -> date. How???
}
case _ => None
}
}
}
The method listFiles(path: String) returns a Seq[String] of absolute path of all files in the path passed as argument to the function
Try to write more idiomatic Scala. Something like this:
val directoryToFile = (for {
path <- awsClient.listFiles(uploadPath)
if !path.endsWith("/")
regex(dir, date) <- regex.findFirstIn(path)
} yield dir -> date).sortBy(_._2).toMap
You can filter and then foldLeft:
val l = List("""/opt/file1.txt""", """/opt/file2.txt""")
val finalMap = l
.filter(!_.endsWith("/"))
.foldLeft(Map.empty[String, LocalDateTime])((map, s) =>
s match {
case regex(dir, date) => map + (dir -> date)
case _ => map
}
)
You can try something like this:
val regex = """(\d)-(\d)""".r
val paths = List("1-2", "3-4", "555")
for {
// Hint to Scala to produce specific type
_ <- Map("" -> "")
// Not sure why your !path.endsWith("/") is not part of regex
path#regex(a, b) <- paths
if path.startsWith("1")
} yield (a, b)
//> scala.collection.immutable.Map[String,String] = Map(1 -> 2)
Slightly more complicated if you need max:
val regex = """(\d)-(\d)""".r
val paths = List("1-2", "3-4", "555", "1-3")
for {
(_, ps) <-
( for {
path#regex(a, b) <- paths
if path.startsWith("1")
} yield (a, b)
).groupBy(_._1)
} yield ps.maxBy(_._2)
//> scala.collection.immutable.Map[String,String] = Map(1 -> 3)

Get ClassTag from reflected Java Class instance

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?
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.
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
}

Categories

Resources