Shifting Into Overdrive: Email Templating Example
It is almost guaranteed that the systems you work on need to
send automated emails to the users, either as receipts for
online purchases or for system-event notifications. These emails
are very likely dynamic, such as the case of an email receipt
listing the items purchased, their costs, and an order number.
There are many technical solutions to this problem, including using
JSP or tokenized regular-expression substitutions. Velocity, of
course, is the recommended solution here, as it provides all of the needed
flexibility and a straightforward template language allowing
even your end users customization capabilities.
This example is going to pull out all of the stops illustrating
the majority of Velocity's capabilities. Our user story is this:
Implement a Java method that accepts an Order
and sends a receipt email to the customer. The email format
must allow easy runtime customization.
From a top-down approach, our method interface is:
public void sendReceipt(Order order) throws Exception
An Order is a Java object encapsulating a
Customer, order line items (a collection of Items),
order number generation, and a method to compute the order total.
import java.util.List;
import java.util.Iterator;
import java.util.Date;
public class Order {
private Customer customer;
private List lineItems;
private String orderNumber;
public Order (Customer customer, List lineItems) {
this.customer = customer;
this.lineItems = lineItems;
// for example purposes, "generate" a simple order number.
orderNumber = customer.getId() + "-" + new Date().getTime();
}
public Customer getCustomer() {
return customer;
}
public List getLineItems() {
return lineItems;
}
public String getOrderNumber() {
return orderNumber;
}
public float total() {
float total = 0;
for (Iterator iterator = lineItems.iterator(); iterator.hasNext();) {
Item item = (Item) iterator.next();
total += item.getCost();
}
return total;
}
}
Customer and Item are fairly typical
JavaBeans, shown below.
public class Customer {
private String firstName;
private String lastName;
private String email;
public Customer (String firstName, String lastName, String email) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public String getEmail() {
return email;
}
/**
* For demonstration purposes, an id is a concatenation of
* first and last initials
*/
public String getId() {
return "" + firstName.charAt(0) + lastName.charAt(0);
}
}
public class Item {
private String description;
private float cost;
public Item(String description, float cost) {
this.description = description;
this.cost = cost;
}
public String getDescription() {
return description;
}
public float getCost() {
return cost;
}
}
Now that we've got the underlying details out of the way, the
Emailer class usage becomes:
/**
* Example usage of Emailer functionality
*/
public static void main(String[] args) throws Exception {
Emailer emailer = new Emailer();
ArrayList lineItems = new ArrayList();
lineItems.add(new Item("Java Development with Ant", 44.95f));
lineItems.add(new Item("Lucene in Action", 41.37f));
Customer customer = new Customer("Duke", "Jahvah", "duke@java.net");
emailer.sendReceipt(new Order(customer, lineItems));
}
Still no view of Velocity -- this is an intentional design
decision, to keep things decoupled and cohesive. The Emailer
class itself fully encapsulates the use of the Velocity API.
Velocity is transparent to developers using Emailer, except for the need
to create a corresponding template. Before we proceed deeper
into the code, we need to first analyze the end goal, an actual
email. Here is an example email:
Duke,
Thank you for your purchase.
Your order number is DJ-1070292605890.
Description Cost
Java Development with Ant $44.95
Lucene in Action $41.37
Total $86.32
Visit us again at http://java.net!
Seeing an actual email body gives us some insight into some
implementation details. First, note some formatting concerns.
The description is output in a fixed-width style. Cost and total
are formatted as currency. If our system is designed to service
multiple stores, store information such as the URL in the last line
perhaps should be provided dynamically into the context rather than
being fixed text in the template. One final foreshadowing of the
issues to address: what about the subject of the email? Shouldn't
this be customizable using the same type of templating?
Continuing the outside-in zoom into Emailer, we see the Emailer
constructor and the sendReceipt method.
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
import java.util.ArrayList;
import java.util.Properties;
import java.util.HashMap;
import java.io.StringWriter;
public class Emailer {
VelocityEngine engine = new VelocityEngine();
public Emailer() throws Exception {
configure(engine);
}
/**
* "Sends" (actually writes to System.out for demonstration
* purposes) a receipt e-mail for the specified order.
*/
public void sendReceipt(Order order) throws Exception {
Template template = engine.getTemplate("email.vm");
VelocityContext context = createContext();
context.put("order", order);
StringWriter writer = new StringWriter();
template.merge(context, writer);
writer.close();
System.out.println("To: " + order.getCustomer().getEmail());
System.out.println("Subject: " + context.get("subject"));
System.out.println(writer.getBuffer());
}
// ...configure and createContext coming soon...
}
A few new tricks are introduced here. First, VelocityEngine
is used, rather than the singleton Velocity seen in the
earlier examples. VelocityEngine is an instance-based way to
invoke the templating merge, keeping configuration separate from
other instances, whereas the singleton does not. The template is
external to our code (more on this later). Also of note is
the call to context.get("subject"). The context is
not a one-way "push," thus allowing the template to inject items back
into it. In this case, our template pushes an email "subject" string
into the context and the sendReceipt method retrieves it. It is
handy to keep the subject and body of an email close together,
and placing them both in the same template allows for customization
of both the subject and body in one spot.
Configuration of Resource Loaders
The trickiest thing when working with Velocity is configuration.
Several configuration options are available with Velocity. For the
Emailer example, Velocity needs to know how to find the template.
Velocity has a resource loader abstraction with built-in
loaders to retrieve templates from the file system, the classpath,
or a data source. Custom resource loaders could be written to
retrieve templates in a way custom to your architecture if needed.
Our email.vm (.vm for Velocity macro) template is not hardcoded
into our source code. Rather, it lives as a file on the classpath.
Velocity can be configured either through the API, or through
a velocity.properties file. I prefer controlling it through the
API to avoid the issue of where to put the velocity.properties file.
Most of Velocity's documentation, however, will show configuration
using the properties-file syntax. Configuring through the API
is a simple translation; here is the properties file equivalent
of the configure method:
resource.loader=classpath
classpath.resource.loader.class=org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader
Using some provided constants, the same configuration is
achieved using the API. The term classpath in the configuration
examples is an arbitrary name used to tie the classname (and potentially
other configuration) to a specific loader.
/**
* Configures the engine to use classpath to find templates
*/
private void configure(VelocityEngine engine) throws Exception {
Properties props = new Properties();
props.setProperty(VelocityEngine.RESOURCE_LOADER, "classpath");
props.setProperty("classpath." + VelocityEngine.RESOURCE_LOADER + ".class",
ClasspathResourceLoader.class.getName());
engine.init(props);
}
Refer to the Velocity Developer's Guide for more details
on configuring resource loaders and other parameters.
Emailer Context
To put the final touches on our Emailer class, our context
is created with more than just the Order object. In order to
give our template control over formatting (rather than forcing our
Java code to deal with it less flexibly), a formatter tool is
placed in the context. General store information is injected
into the context as a Map.
/**
* Creates a Velocity context and adds a formatter tool
* and store information.
*/
private VelocityContext createContext() {
VelocityContext context = new VelocityContext();
context.put("formatter", new Formatter());
HashMap store = new HashMap();
store.put("name", "java.net Bookstore");
store.put("url", "http://java.net");
context.put("store", store);
return context;
}
Formatter is the final Java code to show before
we get to the template. Two formatting functions are needed: padding
or truncating a string to fixed width, and converting a float
amount into a pleasant, locale-dependent currency display.
import java.text.Format;
import java.text.NumberFormat;
public class Formatter {
public String currency(float amount) {
Format formatter = NumberFormat.getCurrencyInstance();
return formatter.format(new Float(amount));
}
public String pad(String string, int width) {
if (string.length() >= width) {
return string.substring(0, width);
}
StringBuffer output = new StringBuffer(string);
for (int i=0; i < (width - string.length()); i++) {
output.append(' ');
}
return output.toString();
}
}
Nearing the Finish Line: The Email Template
Even though we're on our last lap, keep your seat belts
fastened. The email.vm template utilizes several VTL directives,
including the cool #macro. A detailed
analysis of the template follows.
1. #set ($customer = ${order.customer})
2. #macro(currency $amount)${formatter.currency($amount)}#end
3. #macro(pad $string)${formatter.pad($string, 30)}#end
4. #macro(description $item)#pad($item.description)#end
5.
6. ${customer.firstName},
7.
8. Thank you for your purchase.
9. Your order number is ${order.orderNumber}.
10.
11. #pad("Description") Cost
12. #foreach ($item in ${order.lineItems})
13. #description($item) #currency(${item.cost})
14.
15. #end
16.
17. #pad("Total") #currency($order.total())
18.
19.
20. Visit us again at ${store.url}!
21.
22. #set ($subject="${store.name} receipt")
The line numbers on the left are not part of the original
template, but rather for discussion purposes. Lines 6 through 20
make up the email body. The only objects in the
Velocity context are order, store, and formatter.
Line 6 refers to customer.firstName. The customer object
was created on Line 1 using #set. It is merely there
for convenience, simplifying access to the customer object that
is nested within the order object. First name could also be
displayed using ${order.customer.firstName}.
Column headings are generated on Line 11. To keep things
aligned, the #pad macro is defined on Line 3, wrapping
the Formatter.pad method invocation. This keeps
the width in one place within the template. The item descriptions
are also padded, but they go through a #description
wrapper around #pad that is Item aware. Alternatively,
#pad could have been used on Line 13, like #pad($item.description).
Currency is formatted on both Lines 13 and 17. On Line 17,
the Order.total() method is invoked.
The URL to the store, rather than being fixed in the template,
comes from the store context object. This context object is
a Map that contains a url-named entry. Pleasantly, the template
deals with object properties and map entries identically, so it is
possible to switch the underlying implementation of the context object
without affecting the template.
And finally, Line 22 performs the trick previously mentioned,
injecting a subject object into the context, which is retrieved
after the merge in Emailer.sendReceipt.
White space is always an issue with templating engines. Velocity
does some intelligent things to collapse white space, but
experience shows that experimentation is needed to tweak a template
into generating the exact desired output.
The #macro and #set directives on Lines 1-4 and 22 do not directly cause any output during the merge. Notice that the #macro definitions are completely collapsed to avoid them generating
undesirable spaces when used later in the template.
The blank Line 14 is needed to put each item on a separate line.
Lane Ends, Merge Right
This has been a speedy look at Velocity, yet all of the
major pieces have been covered. Adding Velocity to your technical
toolkit is highly recommended. A general-purpose templating engine
comes in handy in many aspects of development, and Velocity is the
best one for the Java language. This look at Velocity covered one
primary use, generating automated emails; there are many other
uses, which can be extrapolated from the examples provided here.
For example, generating HTML output from a web application using
Velocity merely involves morphing the code shown in Emailer into
a servlet (but refer to Resources before doing so, as several
such implementations are already available). More details on Velocity's
syntax, API, and configuration were intentionally omitted from
this article, since these are covered in Velocity's excellent provided
documentation.
Resources
Indispensable companions to this article are Velocity's
User's Guide,
Developer's Guide,
VTL Reference Guide,
and API Reference.
Many useful tools and wrapper frameworks, including tight integration
with Struts, are included in the
VelocityTools.
Many Struts developers are replacing the typical JSP view with Velocity templates.
Velocity is used as a first-class templating solution in
the code-generation tools Middlegen and
XDoclet2.
Megg uses Velocity
templates to generate complete starter Java project infrastructures.
VPP provides a powerful templating solution
during your build process, to generate, for example, environment-specific
configuration files. The VPPFilter handily beats the clunkier
<filterset> token replacements to which you may be accustomed.
Mastering Apache Velocity (Wiley)
Erik Hatcher is the co-author of the premiere book on Ant, Java Development with Ant (published by Manning), and is co-author of "Lucene in Action".