Thursday, January 10, 2008

Velocity Variables Persisting Across Templates

Yesterday, I noticed that the Velocity template engine used in Rhizome was behaving strangely. Variables set using the velocity #set directive were persisting over multiple calls to VelocityEngine.mergeTemplate().

So if I set a variable on pageA (using templateA), and then requested pageB (using a different template), the variable would still be available. This has some nasty consequences -- especially considering that Velocity (by default) will not assign nulls to variables.

Template A:

#set($title= 'My Title');

Template B:
#set($title = $myObj.getTitle())

What happens if getTitle() returns null? This bit is less than intuitive: Velocity ignores the assignment altogether.

So the combination of the variable context bleeding and the null assignment behavior is that if template A is rendered on a first request, and then template B is used on the next request, the title for template B will be
'My Title'.

Surely, this is not good behavior. So what is going on?

First, the Null behavior can be configured. Second, the problem with the variable bleeding is a result of some interesting Velocity internals dealing with VelocityContext objects. Let's look at some Java pseudocode:

VelocityEngine ve = new VelocityEngine();
VelocityContext cxt = newVelocityContext(myHashMap);
ve.mergeTemplate(myTemplateFile,"UTF-8", cxt, myWriter);

In the three lines above, we create a velocity engine, create a context, and then merge a single template.

Line 2 is the important one for us: When Velocity merges a template, it uses the context for storing some variable information. If it finds a Map inside of the context, it uses that Map for storing variables, assuming the Map will be discarded after the template merge.

What if the Map is static? You guessed it: All variables will be stored in the same Map. Bad.

The solution, as it turns out, is fairly simple -- though a bit odd: Remove the Map from the immediate context. This can be done with the Context Chaining feature of Velocity's contexts:

VelocityContext cxt = new VelocityContext(new VelocityContext(myHashMap));

(Alternately, you could just clone the Map into a non-static instance. The above should be less resource intensive, caveat emptor).

Finally, it is also easy (in Velocity 1.5 and later, at least) to get rid of the null assignment behavior. Simply set the SET_NULL_ALLOWED property to true:
ve.setProperty(VelocityEngine.SET_NULL_ALLOWED, true);
These changes are now checked into Rhizome's repository.