Now for a somewhat more realistic example, a guestbook servlet. You can
run it in the same way as the previous example. To build this example,
from the examples
directory, type:
ant guestbook
As before, this will create a deploy
directory under the
examples/guestbook
directory. You can now deploy the contents
of this directory to your application server. You can then invoke the servlet
with something like http://localhost:8080/guestbook/
.
Here's the template for the guestbook servlet:
<html> <head> <title>Guestbook</title> </head> <body bgcolor="#e0e0ff"> <div align="center"> <font face="verdana,arial,helvetica" size="6" color="#191970"><b>Guestbook</b></font> </div> <br><br><br> <if entryAdded> <p>Thank you for your entry.</p> <p><a href="/guestbook">Add another entry</a></p> <else> <p>Enter your name and message:</p> <form action="/guestbook" method="post"> <input type="hidden" name="dataSubmitted" value="true"> <table border="0"> <tr> <td align="right">Name:</td> <td><input type="text" name="guestName" size="20"></td> </tr> <tr> <td align="right" valign="top">Message:</td> <td><textarea name="message" wrap="virtual" cols="50" rows="5"></textarea></td> </tr> </table> <p><input type="submit" value="Submit"></p> </if> <hr> <if guestbook> <br> <table border="1" cellspacing="0" cellpadding="5" width="100%"> <tr> <td width="10%" align="left" bgcolor="#f0f0ff"> <font face="verdana,arial,helvetica" size="+1" color="#191970"> <b>Number</b> </font> </td> <td width="20%" align="center" bgcolor="#f0f0ff"> <font face="verdana,arial,helvetica" size="+1" color="#191970"> <b>Date</b> </font> </td> <td width="20%" align="center" bgcolor="#f0f0ff"> <font face="verdana,arial,helvetica" size="+1" color="#191970"> <b>Name</b> </font> </td> <td width="50%" align="center" bgcolor="#f0f0ff"> <font face="verdana,arial,helvetica" size="+1" color="#191970"> <b>Message</b> </font> </td> </tr> <list guestbook as entry> <tr> <td bgcolor="#f0f0ff" valign="top">${entry.number}</td> <td bgcolor="#f0f0ff" valign="top">${entry.date}</td> <td bgcolor="#f0f0ff" valign="top">${entry.name}<br></td> <td bgcolor="#f0f0ff" valign="top">${entry.message}<br></td> </tr> </list> </table> <p> <font size="-1"><i>First Entry:</i><br> Name is ${guestbook[0].name}<br> Message is ${guestbook[0].message}<br> Date is ${guestbook[0].date}<br> </font> </p> <else> <p>There are no guestbook entries.</p> </if> </body> </html> |
The easiest way to make a template data model would be to have the servlet
store the guestbook entries in a SimpleList
; each entry would be
a SimpleHash
of two SimpleScalar
s (name and message).
In many cases, this approach is perfectly adequate. However, suppose we also
want to be able to display our guestbook in some other form, e.g. in an applet.
We can make the underlying guestbook classes as generic as possible, and write
thin TemplateModel
classes that provide an interface between
them and the template. We can adapt Model/View/Controller architecture to
this situation: the template is the view, and the servlet is the controller.
The servlet is responsible for passing input to the generic model objects,
connecting them to TemplateModel
classes, putting the latter in
a TemplateModelRoot
, and processing the template.
We could take one of two approaches to designing our
TemplateModel
classes. In the first approach, we would give them
the same interface as the generic model classes, and pass them the user's
input; in this case, we would consider them to be wrapper (or "decorator")
classes. In the second approach, we would give them only the interface that
they need to have as TemplateModel
s, and pass them the generic
model objects only after these have been initialized and given user input;
we would then consider our TemplateModel
classes to be adapter
classes. In this example, we will take the adapter approach.
An object that has named properties needs an adapter that implements
TemplateHashModel
; an object that represents a list of elements
needs an adapter that implements TemplateListModel
.
First, we'll need a generic model class Guestbook
, which
maintains a list of GuestbookEntry
objects:
package examples.guestbook; import java.util.*; public class Guestbook { private final List<GuestbookEntry> list = Collections.synchronizedList(new LinkedList<GuestbookEntry>()); public Guestbook() { } public void addEntry(String name, String message) { list.add(new GuestbookEntry(list.size() + 1, name, message)); } public List<GuestbookEntry> getList() { return list; } } |
package examples.guestbook; import java.util.Date; public class GuestbookEntry { private final Date date; private final String name; private final String message; private final int number; public GuestbookEntry(int number, String name, String message) { date = new Date(); this.name = name; this.message = message; this.number = number; } public int getNumber() { return number; } public Date getDate() { return date; } public String getName() { return name; } public String getMessage() { return message; } } |
We can now make the TemplateModel
adapters for these
classes:
package examples.guestbook; import freemarker.template.*; import java.util.*; public class GuestbookTM implements TemplateListModel2, TemplateIndexedModel { private final List<GuestbookEntry> entries; public GuestbookTM(Guestbook book) { this.entries = book.getList(); } public boolean isEmpty() throws TemplateModelException { return entries.isEmpty(); } public TemplateIteratorModel templateIterator() throws TemplateModelException { return new GuestbookIterator( entries.iterator() ); } public void releaseIterator(TemplateIteratorModel iterator) { } public TemplateModel getAtIndex(long i) throws TemplateModelException { if ((i >= 0) && (i < entries.size())) { return new GuestbookEntryTM(entries.get((int)i)); } else { throw new TemplateModelException("Index out of range."); } } } |
package examples.guestbook; import freemarker.template.*; import java.util.*; public class GuestbookIterator implements TemplateIteratorModel { private final Iterator<GuestbookEntry> iterator; public GuestbookIterator(Iterator<GuestbookEntry> iterator) { this.iterator = iterator; } public boolean isEmpty() throws TemplateModelException { return false; } public boolean hasNext() throws TemplateModelException { return iterator.hasNext(); } public TemplateModel next() throws TemplateModelException { if (iterator.hasNext()) { return new GuestbookEntryTM(iterator.next()); } else { throw new TemplateModelException("No more elements."); } } } |
Our adapter for GuestbookEntry
can read its
GuestbookEntry
's values as needed, store them in objects
implementing TemplateScalarModel
or TemplateNumberModel
,
and store these as instance variables of its own. Since the values are just
String
, int
and Date
objects, they can
conveniently be stored in SimpleScalar
and SimpleNumber
objects, once the Date
object is properly formatted for output.
package examples.guestbook; import freemarker.template.*; import java.util.TimeZone; import java.text.DateFormat; public class GuestbookEntryTM implements TemplateHashModel { private final GuestbookEntry entry; private SimpleNumber number; private SimpleScalar date; private SimpleScalar name; private SimpleScalar message; public GuestbookEntryTM(GuestbookEntry entry) { this.entry = entry; } public TemplateModel get(String key) throws TemplateModelException { if ("number".equals(key)) { return getNumber(); } else if ("date".equals(key)) { return getDate(); } else if ("name".equals(key)) { return getName(); } else if ("message".equals(key)) { return getMessage(); } else { return null; } } private TemplateModel getNumber() { if (number == null) { number = new SimpleNumber(entry.getNumber()); } return number; } private TemplateModel getDate() { if (date == null) { DateFormat dateFormat = DateFormat.getDateTimeInstance( DateFormat.MEDIUM, DateFormat.LONG); dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); date = new SimpleScalar(dateFormat.format(entry.getDate())); } return date; } private TemplateModel getName() { if (name == null) { name = new SimpleScalar(entry.getName()); } return name; } private TemplateModel getMessage() { if (message == null) { message = new SimpleScalar(entry.getMessage()); } return message; } public boolean isEmpty() throws TemplateModelException { return (entry == null); } } |
Our servlet will store its single template,
guestbook_template.html
, in a Cache
so it will be automatically reloaded when changed. Cache
objects are explained here. The servlet will rely
on its environment to provide it with the path of its template directory,
and the suffix _template.html
of its template file.
package examples.guestbook; import java.io.*; import javax.servlet.*; import javax.servlet.http.*; import freemarker.ext.misc.HtmlEscape; import freemarker.template.*; public class GuestbookServlet extends HttpServlet implements CacheListener { private Guestbook book; private FileTemplateCache templateCache; private String templateSuffix; public void init(ServletConfig config) throws ServletException { super.init(config); String templatePath = getInitParameter("templatePath"); if (templatePath == null) { throw new UnavailableException(this, "Init parameter templatePath not set."); } templateSuffix = getInitParameter("templateSuffix"); if (templateSuffix == null) { throw new UnavailableException(this, "Init parameter templateSuffix not set."); } String realPath = getServletContext().getRealPath(templatePath); templateCache = new FileTemplateCache(realPath); templateCache.setFilenameSuffix(templateSuffix); templateCache.addCacheListener(this); book = new Guestbook(); } public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType("text/html"); PrintWriter out = new PrintWriter(res.getOutputStream()); // Get the guestbook's template. Template template = getTemplate("guestbook" + templateSuffix); // Make the root node of the data model. SimpleHash modelRoot = new SimpleHash(); // If an entry was submitted, add it to the guestbook. if (req.getParameter("dataSubmitted") != null) { book.addEntry(req.getParameter("guestName"), req.getParameter("message")); modelRoot.put("entryAdded", new SimpleScalar(true)); } // Wrap the guestbook in a template model adapter. GuestbookTM bookTM = new GuestbookTM(book); modelRoot.put("guestbook", bookTM); // Process the template. TemplateProcessorParameters tpp = TemplateProcessorParameters.newInstance(out) .withModel(modelRoot) .withEscape(HtmlEscape.getInstance()); template.process(tpp); out.close(); } public void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { doGet(req, res); } private Template getTemplate(String templateName) throws ServletException { Template template = (Template)templateCache.getItem(templateName); if (template == null) { throw new ServletException("Template " + templateName + " is not available."); } else { return template; } } public void cacheUnavailable(CacheEvent e) { System.out.println("Cache unavailable: " + e.getException().toString()); } public void elementUpdated(CacheEvent e) { System.out.println("Template updated: " + e.getElementName()); } public void elementUpdateFailed(CacheEvent e) { System.out.println("Update of template " + e.getElementName() + " failed: " + e.getException().toString()); } public void elementRemoved(CacheEvent e) { System.out.println("Template removed: " + e.getElementName()); } public String getServletInfo() { return "Guestbook Servlet"; } } |
Previous: Hello World | Next: Template Caches |