Sample Code

Guestbook

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 SimpleScalars (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 TemplateModels, 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";
    }
}