A (very) Simple Template Renderer for Java
When we introduced customisable email subjects for Reminders for Bitbucket I needed a template renderer that
- takes a user-defined string based template
- references template placeholders and resolves their values in the render context
- does not depend on any heavy weight templating library
Let’s assume the end-user would enter a email subject template like
This is PR #{pullRequest.id} from {pullRequest.author.user.name}
Placeholders are marked by {} and reflect the Java object’s hierarchy. Yes – reflect – is the key word here. The impatient may find a ready to go class on Github.
A (very) Simple Expression Parser
To identify the placeholders in the template we use a regex pattern matching like
List<String> expressions = Lists.newArrayList(); Matcher m = Pattern.compile("\\w*\\{(.*?)\\}").matcher(subject); while (m.find()) { String expression = m.group(1); expressions.add(expression); }
Finding the Placeholder Values
The render context holds all the Java objects needed to resolve the placeholder expression values and turn them into Strings.
Stream<String> values = expressions.stream() .map(e -> resolve(context, getMethods(e))) .map(v -> v+"");
Resolving Expressions
As mentioned above Java Reflection is used to traverse the render context objects via their getter and setter methods. First a list of methods is extracted from the placeholder expression.
Arrays.stream(expression.split("\\.")) .map(m -> "get" + m.substring(0, 1).toUpperCase() + m.substring(1))
For example {pullRequest.id} would be resolved to [getPullRequest(), getId()], and {pullRequest.author.user.name} would be resolved to [getPullRequest(), getAuthor(), getUser(), getName()].
The obtained list of methods will then be called recursively on the render context object using Java reflection in order to obtain the target value.
private Object resolve(Object o, List<String> methods) { if (methods.size() == 0) return o; Object result = null; try { Method m = o.getClass().getMethod(methods.get(0)); result = m.invoke(o); } catch (NoSuchMethodException e) { ... } methods.remove(0); return resolve(result, methods); }
Rendering the Result
Now that we have a list of values that substitute their placeholders let’s rewrite the template into a format String
String format = templateString.replaceAll("\\{.*?\\}", "%s");
… and use String.format() to render the final result
String.format(format, values.toArray());
Get the complete class from Github.