File: scala-model-adaptor.md

package info (click to toggle)
stringtemplate4 4.3.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,772 kB
  • sloc: java: 15,537; xml: 305; python: 39; makefile: 2
file content (167 lines) | stat: -rw-r--r-- 6,301 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# A Model Adaptor for Scala Case Classes

One issue with the original adaptors is that they fail to access Scala case classes. In Scala, case classes of the following form are extremely convenient to use: 

```scala
  case class Point(x: Int, y: Int)
  case class Triangle(p1: Point, p2: Point, p3: Point)
```

Below, we have included one implementation of an adaptor that works with the aforementioned Scala case classes. The adaptor uses Scala reflection to fetch the desired property, and then converts it back to Java before passing it along to the interpreter. Of note, there is also a handy `register()` method in the companion object that allows for quicker and less verbose registering of a new adaptor to an `STGroup `. 

Note also that an adaptor for each case class (and each sub-class) must be registered for the template to render correctly. For example, to use the `Triangle` case class from above, you would have to register an adaptor for both the `Point` and `Triangle` classes.
```scala
import org.stringtemplate.v4._
import org.stringtemplate.v4.misc._

import scala.collection.mutable
import scala.reflect.ClassTag
import scala.reflect.runtime.universe._

trait ToJava {
  import scala.collection.JavaConverters._

  /**
    * Recursively converts to java object that is usable by ST4
    * Note: For Options, None is converted to empty string ("")
    */
  def toObject(o: Any): Object = {
    o match {
      case opt: Option[_] => opt.fold[Object]("")(toObject)
      case map: Map[_, _] => map.map {
        case (k, v) => toObject(k) -> toObject(v)
      }.asJava
      case it: Iterable[_] => it.map(toObject).asJava
      case obj: AnyRef => obj
      case primitive => primitive.asInstanceOf[Object]
    }
  }
}

// A Scala-friendly adaptor for use with ST4
class ScalaModelAdaptor[T: TypeTag: ClassTag] extends ObjectModelAdaptor[T] with ToJava {
  import ScalaModelAdaptor._

  // stores seen fields/methods in mirrors for future reference
  private val mirrorCache = mutable.Map.empty[String, MethodMirror]

  // tells ST4 how to get the fields from an object of type T
  override def getProperty(interp: Interpreter, self: ST, model: T, property: Any, propertyName: String): Object = {
    mirrorCache.get(propertyName) match {
      case Some(mirror) => toObject(mirror.bind(model).apply())
      case _ =>
        typeOf[T].member(TermName(propertyName)) match {
          case NoSymbol => "" // skip over mismatched properties
          case refl =>
            val mirror = getMirror(refl, model)
            mirrorCache.put(propertyName, mirror)
            toObject(mirror.apply())
        }
    }
  }
}

object ScalaModelAdaptor {
  import scala.reflect.classTag

  // Registers a new ScalaModelAdaptor to a given STGroup
  def register[T: TypeTag: ClassTag](st: STGroup): STGroup = {
    val adaptor = new ScalaModelAdaptor[T]
    st.registerModelAdaptor(classTag[T].runtimeClass.asInstanceOf[Class[T]], adaptor)
    st
  }

  private def getMirror[T: TypeTag: ClassTag](refl: Symbol, model: T): MethodMirror = {
    scala.reflect.runtime.universe
      .runtimeMirror(getClass.getClassLoader)
      .reflect(model)
      .reflectMethod(refl.asMethod)
  }
}

// and a representative test:
object STest {  
  case class Point(x: Int, y: Int)
  case class PointClss(name: String, pointList: List[Point], pointMap: Map[Int, Point], pointSet: Set[Point], pointLL: List[List[Point]])

  val p = Point(4, 5)
  val q = Point(1, 6)
  val r = Point(3, 2)
  val pl = PointClss("Point Test", List(p, q ,r), Map(1 -> p, 2 -> q, 3 -> r), Set(p, q, r), List(List(p, q), List(q, r)))

  val group = {
    val template =
      """
        |pointPrinter(p) ::= "(<p.x>, <p.y>)"
        |
        |mapKeyVal(m) ::= <<
        |{<m.keys:{k | <k> -> <m.(k)>}; separator = ", ">}
        |>>
        |
        |ll(p) ::= <<
        |[<p:pointPrinter(); separator = ", ">]
        |>>
        |
        |test(t) ::= <<
        |<t.name>: 
        |List: [<t.pointList:pointPrinter(); separator = ", ">];
        |Map: <mapKeyVal(t.pointMap)>;
        |Set: {<t.pointSet:pointPrinter(); separator = ", ">};
        |List of list: [<t.pointLL:ll(); separator = ", ">]
        |>>
        |""".stripMargin
    val g = new STGroupString(template)

    ScalaModelAdaptor.register[PointClss](g)
    ScalaModelAdaptor.register[Point](g)
  }

  group.getInstanceOf("test").add("t", pl).render()

  /* should print: 
  Point Test:
  List: [(4, 5), (1, 6), (3, 2)];
  Map: {1 -> Point(4,5), 2 -> Point(1,6), 3 -> Point(3,2)};
  Set: {(4, 5), (1, 6), (3, 2)};
  List of list: [[(4, 5), (1, 6)], [(1, 6), (3, 2)]]
  */
}
```

## Advanced Customization for More Specific Adaptors

You can also extend this adaptor and override the `getProperty()` function to encode functionality that is specific to a certain case class. If the `toObject()` method is needed in the `getProperty()` override (for example, special casing with iterables, maps, or options), the new class can be extended with the `toJava` trait. For example, if you wanted to modify how the `x` and `y` values of the `Point` class outputted, you could write it like this: 

```scala
class PointAdaptor extends ScalaModelAdaptor[Point] with ToJava {
  override def getProperty(interp: Interpreter, self: ST, model: Point, property: Any, propertyName: String): Object = {
    propertyName match {
      case "x" => s"x: ${super.getProperty(interp, self, model, property, propertyName)}"
      case "y" => s"y: ${super.getProperty(interp, self, model, property, propertyName)}"
      case "x_times" =>
        val x = super.getProperty(interp, self, model, property, "x").asInstanceOf[Int]
        toObject(Seq.tabulate(x)(_ => model))
      case _ => super.getProperty(interp, self, model, property, propertyName)
    }
  }
}

object PointTest extends App {
  val p = Point(2, 5)

  val group = {
    val template =
      """
        |id(x) ::= "<x>"
        |point(p) ::= <<
        |(<p.x>, <p.y>); <p.x_times:id(); separator = ", ">
        |>>""".stripMargin
    val g = new STGroupString(template)
    g.registerModelAdaptor(classTag[Point].runtimeClass.asInstanceOf[Class[Point]], new PointAdaptor)
    g
  }

  group.getInstanceOf("point").add("p", p).render()
  // should render as: (x: 2, y: 5); Point(2,5), Point(2,5)
}
```