Next up on my list of template engines to try with Scala & Spring - Velocity. Unlike FreeMarker, I had actual success in getting it to work. Unsurprisingly, "Native" Scala object support needed to be added in - fortunately the way Velocity was designed makes that easy.
Velocity uses introspection/reflection to access properties and methods on an object and this is where the Scala customizations fit in. The first thing to create is a subclass of
UberspectImpl which does two things - return a java.util.Iterator for iterable objects and return a
VelPropertyGet object for getting access to an object's properties. There is a third thing that could also be done - returning a
VelPropertySet object to support setting properties from Velocity but I haven't implemented this as I don't need that functionality at the moment.
Getting a Java Iterator is fairly straightforward (with Scala 2.7.7 - I'm not sure what, if any, changes will be needed with 2.8)
override def getIterator(obj: java.lang.Object, i: Info): JavaIterator[_] = {
def makeJavaIterator(iter: Iterator[_]) = new JavaIterator[AnyRef] {
override def hasNext() = iter.hasNext
override def next() = iter.next().asInstanceOf[AnyRef]
override def remove() = throw new java.lang.UnsupportedOperationException("Remove not supported")
}
obj match {
case i: Iterable[_] => makeJavaIterator(i.elements)
case i: Iterator[_] => makeJavaIterator(i)
case _ => super.getIterator(obj, i)
}
}
The method to return a VelPropertyGet object is almost as simple - but it also shows that we need two more classes - one for accessing Scala properties and one for accessing Scala Map members like they're properties.
override def getPropertyGet(obj: java.lang.Object, identifier: String, i: Info): VelPropertyGet = {
if (obj != null) {
val claz = obj.getClass()
val executor = obj match {
case m: Map[_, _] => new ScalaMapGetExecutor(log, claz, identifier)
case _ => new ScalaPropertyExecutor(log, introspector, claz, identifier)
}
if (executor.isAlive) {
new VelGetterImpl(executor)
} else {
super.getPropertyGet(obj, identifier, i)
}
} else {
null
}
}
(You can get the full source code
here)
To access Scala style properties I'll need a Scala specific subclass of PropertyExecutor. The code to access properties from Scala objects is once again trivial. In fact, because of the way Scala generates it's getter methods the code to access Scala properties is simpler than the code to access JavaBean style properties. (
source)
override def discover(clazz: java.lang.Class[_], property: String) = {
val params = Array[java.lang.Object]()
setMethod(introspector.getMethod(clazz, property, params))
if(!isAlive()) {
super.discover(clazz, property)
}
}
Compare that to the code in the
parent class
protected void discover(final Class clazz, final String property)
{
/*
* this is gross and linear, but it keeps it straightforward.
*/
try
{
Object [] params = {};
StringBuffer sb = new StringBuffer("get");
sb.append(property);
setMethod(introspector.getMethod(clazz, sb.toString(), params));
if (!isAlive())
{
/*
* now the convenience, flip the 1st character
*/
char c = sb.charAt(3);
if (Character.isLowerCase(c))
{
sb.setCharAt(3, Character.toUpperCase(c));
}
else
{
sb.setCharAt(3, Character.toLowerCase(c));
}
setMethod(introspector.getMethod(clazz, sb.toString(), params));
}
}
/**
* pass through application level runtime exceptions
*/
catch( RuntimeException e )
{
throw e;
}
catch(Exception e)
{
String msg = "Exception while looking for property getter for '" + property;
log.error(msg, e);
throw new VelocityException(msg, e);
}
}
It took me a little while to get Maps working (even though there's not much code to show for it).
class ScalaMapGetExecutor(val llog: Log, val clazz: java.lang.Class[_], val property: String) extends MapGetExecutor(llog, clazz, property) {
override def isAlive = true
override def execute(o: AnyRef) = o.asInstanceOf[Map[String, AnyRef]]
.getOrElse[AnyRef](property, null).asInstanceOf[java.lang.Object]
}
All that's left to do now is configure Velocity from Spring so that it uses the Scala specific Uberspector instead of the default. So, in my servletname-servlet.xml file I needed to add the following
<bean id="velocityConfig" class="org.springframework.web.servlet.view.velocity.VelocityConfigurer">
<property name="resourceLoaderPath" value="/WEB-INF/views/"/>
<property name="velocityProperties">
<props>
<prop key="runtime.introspector.uberspect">info.threebelow.verla.mvc.view.velocity.ScalaUberspect</prop>
</props>
</property>
</bean>