Using a Template Engine from your Java code
Several times we’ve needed to retain output files which are a mix of static HTML and dynamic data.
In some of those cases, we’ve wanted to capture the same HTML (or a subset of it) which was returned to a user’s browser as a normal HTTP response. While that’s made me wish for a programmatic way to call the JSP engine, we ended up using a custom Filter to just capture the HTML on the way out, and that’s worked well enough.
However, we now have a desire to capture merged HTML into standalone files that are not related to HTTP request or response processing. So rather than hardcode the HTML creation, I decided to start using one of the existing template engines. Velocity, FreeMarker, and StringTemplate are 3 of the most used. One of my main requirements was that the template files be true HTML that would syntax-check successfully in an IDE or code editor.
Generic Interface
Without going into how I selected (it’s not real concrete, but there are several comparisons on StackOverflow), I decided to further pursue both Velocity and StringTemplate, so I wanted to start with a generic interface, using their implementations to help me decide which of the two I’d use. Remember, we’re not using a template engine to replace our JSP/JSTL “view” rendering, just for merging static & dynamic data to create a handful of specific files we need to retain.
import java.util.Map;
/**
* Merge static "templates" with dynamic data. Implementations could be custom or could make use
* of 3rd-party template engines.
*/
public interface TemplateMerger
{
/**
* Merge a group of data objects into a template, specified by name.
* @param templateName name of an object (e.g. file) which contains the template to be filled
* @return Merged data
* @throws MergeException if the merge fails for any reason (e.g. IOException reading a file).
* Implementations should probably instead throw RuntimeExceptions for cases which are
* caused by coding bugs, such as specifying a particular template but failing to provide
* a data object required by that template.
*/
String merge(String templateName, Map<String, ?> <string, ?="">dataObjects) throws MergeException;
/**
* Merge a group of data objects into a pattern specified in a String literal.
* @param pattern necessarily specific to the TemplateMerger implementation
* @return Merged data
*/
String mergeLiteral(String pattern, Map<String, ?> <string, ?="">dataObjects);
}
In the end, I had to choose, and for various reasons I chose Velocity. Here are both versions, although the StringTemplate version is a little less capable because I stopped working with it after I chose Velocity.
StringTemplate Implementation
import java.util.Map;
import org.stringtemplate.v4.ST;
import org.stringtemplate.v4.STRawGroupDir;
/**
* Implementation using StringTemplate Template Engine.
* @see http://www.stringtemplate.org/
*/
public class StringTemplateMerger implements TemplateMerger
{
private String dirName;
private STRawGroupDir dir;
public void init() {
// STRawGroupDir, allowing plain HTML files as templates, didn't appear until version 4.0.5
// http://www.antlr.org/wiki/display/ST4/4.0.5+Release+Notes
dir = new STRawGroupDir(getDirName(), '$', '$');
}
public String merge(String templateName, Map<String, ?> dataObjects) throws MergeException {
ST template = dir.getInstanceOf(templateName + ".html");
if (template == null) {
throw new MergeException();
}
addDataObjects(dataObjects, template);
return template.render();
}
public String mergeLiteral(String pattern, Map<String, ?> dataObjects) {
ST template = new ST(pattern, '$', '$');
addDataObjects(dataObjects, template);
return template.render();
}
protected void addDataObjects(Map<String, ?> dataObjects, ST template) {
for (Map.Entry data : dataObjects.entrySet()) {
template.add(data.getKey(), data.getValue());
}
}
public String getDirName() {
return dirName;
}
/**
* StringTemplate loads template files from a base directory which can be either a regular File
* directory or a Classpath Resource directory. It will first look for a directory File from the
* specified directory name, and if that doesn't exist try the classpath.
*/
public void setDirName(String dirName) {
this.dirName = dirName;
}
}
Velocity Implementation
One of the reasons I chose Velocity was that I could use the existing VelocityTools extensions to, for instance, format Date values in different ways, so you can see my use of the DateTool object in this implementation.
import java.io.File;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.TimeZone;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.exception.ResourceNotFoundException;
import org.apache.velocity.tools.generic.DateTool;
/**
* Implementation using the Apache Velocity engine.
* @see http://velocity.apache.org/
*/
public class VelocityMerger implements TemplateMerger
{
private String dirName;
/** For date formatting */
private TimeZone timeZone;
private Map<String, String> dateToolConfig = new HashMap<String, String>();
public void init() {
File dir = new File(getDirName());
if (dir == null || !dir.exists() || !dir.isDirectory() || !dir.canRead()) {
throw new RuntimeException("Template Directory " + getDirName() + " is invalid.");
}
Properties properties = new Properties();
properties.setProperty(Velocity.FILE_RESOURCE_LOADER_PATH, getDirName());
Velocity.init( properties );
}
public String merge(String templateName, Map<String, ?> dataObjects) throws MergeException {
try {
Template template = Velocity.getTemplate(templateName + ".html");
VelocityContext ctx = fillContext(dataObjects);
StringWriter out = new StringWriter();
template.merge(ctx, out);
return out.toString();
}
catch (ResourceNotFoundException e) {
throw new MergeException(e.getMessage());
}
}
public String mergeLiteral(String pattern, Map<String, ?> dataObjects) {
VelocityContext ctx = fillContext(dataObjects);
StringWriter out = new StringWriter();
Velocity.evaluate(ctx, out, "mergeLiteral", pattern);
return out.toString();
}
protected VelocityContext fillContext(Map<String, ?> dataObjects) {
VelocityContext ctx = new VelocityContext();
DateTool date = new DateTool();
date.configure(dateToolConfig);
ctx.put("date", date);
for (Map.Entry data : dataObjects.entrySet()) {
ctx.put(data.getKey(), data.getValue());
}
return ctx;
}
public String getDirName() {
return dirName;
}
public void setDirName(String dirName) {
this.dirName = dirName;
}
public TimeZone getTimeZone() {
return timeZone;
}
public void setTimeZone(TimeZone timeZone) {
this.timeZone = timeZone;
dateToolConfig.put(DateTool.TIMEZONE_KEY, timeZone.getID());
}
}