type safe templating

Servlets with Jamon

The goal of this document is to guide the reader through the process of developing and deploying servlets that use Jamon to generate the HTML (i.e., for their view, in MVC parlance). Thus the servlets will be kept simple but nevertheless representative of a typical development environment.

  1. Prerequisites To get the examples described in this document to work you will need
    • Java SDK 1.3.1 or later,
    • a servlet container compliant with version 2.2 (or later) of the servlet specification,
    • Jamon version 1.0 or later, and
    • Ant version 1.4 or later.
  2. Hello World In this section we will create a very simple servlet that displays a page generated using Jamon. To this end we will present
    • the complete code for a servlet,
    • a (Jamon) template,
    • a deployment descriptor (i.e., the web.xml file), and
    • an Ant script to build the project.
    In addition the exact directory layout will be specified, but of course in your own projects you are free to use your own layout. Note that
    • All paths are relative to a base directory for the tutorial. For example, if you choose the base directory to be /home/alice/tutorial, then a reference to a directory templates/foo/bar means /home/alice/tutorial/templates/foo/bar. A reference to a file web.xml means /home/alice/tutorial/web.xml
    • If you wish to avoid typing in the examples in this section all the source code is available in a zipfile.

    In directory templates/foo/bar create a file called HelloTemplate.jamon (you should save this file since your browser may not display it correctly) containing:

    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
    <html>
      <head>
        <title>Hello World</title>
      </head>
      <body>
        Hello world!
      </body>
    </html>
    
    In directory src/foo/bar create a file HelloServlet.java containing:
    /* This Source Code Form is subject to the terms of the Mozilla Public
     * License, v. 2.0. If a copy of the MPL was not distributed with this
     * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
    package foo.bar;
    
    import java.io.IOException;
    
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.jamon.compiler.RecompilingTemplateManager;
    import org.jamon.TemplateManagerSource;
    
    public final class HelloServlet
        extends HttpServlet {
    
        public void init() {
            RecompilingTemplateManager.Data data =
                new RecompilingTemplateManager.Data();
            data.setSourceDir("templates");
            data.setWorkDir("build/work");
            TemplateManagerSource.setTemplateManager(
                new RecompilingTemplateManager(data));
        }
    
        public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException {
            response.setContentType("text/html");
            new HelloTemplate().render(response.getWriter());
        }
    }
    
    Create the following deployment descriptor by placing the following in a file called web.xml:
    <?xml version="1.0" encoding="ISO-8859-1"?>
    
    <!DOCTYPE web-app
        PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/j2ee/dtds/web-app_2_3.dtd">
    
    <web-app>
    
        <servlet>
          <servlet-name>Hello</servlet-name>
          <servlet-class>foo.bar.HelloServlet</servlet-class>
        </servlet>
    
        <servlet-mapping>
            <servlet-name>Hello</servlet-name>
            <url-pattern>/hello</url-pattern>
        </servlet-mapping>
    
    </web-app>
    
    Create an Ant script by placing the following in a file called build.xml:
    <?xml version="1.0"?>
    
    <project name="FooBar" default="war">
    
        <property name="build.dir" value="build"/>
        <property name="build.src.dir" value="${build.dir}/src"/>
        <property name="classes.dir" value="${build.dir}/classes"/>
    
        <target name="templates">
            <mkdir dir="${build.src.dir}"/>
            <taskdef name="jamon" classname="org.jamon.ant.JamonTask" classpathref="maven.runtime.classpath"/>
            <jamon destdir="${build.src.dir}" srcdir="templates"/>
        </target>
    
        <target name="compile" depends="templates">
            <mkdir dir="${classes.dir}"/>
            <javac destdir="${classes.dir}" classpathref="maven.runtime.classpath">
                <src path="src"/>
                <src path="${build.src.dir}"/>
            </javac>
        </target>
    
        <target name="war" depends="compile">
            <mkdir dir="lib"/>
            <copy todir="lib" verbose="true" flatten="true">
                <fileset refid="jamon.libs"/>
                <mapper>
                    <chainedmapper>
                        <flattenmapper/>
                        <globmapper from="jamon-*.jar" to="jamon-*.jar"/>
                    </chainedmapper>
                </mapper>
            </copy>
            <war destfile="foobar.war" webxml="web.xml">
                <classes dir="${classes.dir}"/>
                <lib dir="lib"/>
            </war>
        </target>
    
        <target name="clean">
            <delete dir="${build.dir}"/>
        </target>
    
    </project>
    
    In order to run this script you need to tell Ant where to find the Jamon libraries and the servlet APIs. For example, say the jamon JAR file is in /usr/share/java/jamon.jar and the servlet APIs are in /somewhere/else/servlet.jar, then you would execute it by typing:
    % ant -Djava.lib.jamon=/usr/share/java/jamon.jar      -Djava.lib.servlet=/somewhere/else/servlet.jar
    This will translate the Jamon source into Java, compile them and the servlet, and place it all into a WAR file called foobar.war. (You can download a copy of the WAR file if you like.) Place the WAR file where your servlet container expects to find web applications and start the container. Assuming you deployed the above servlet into a context called "foobar" and that the container listens for HTTP requests on port 8080, you should receive a worldly greeting by pointing a browser to http://localhost:8080/foobar/hello.
  3. Passing parameters and receiving input In this section we elaborate on the previous example by passing the template some parameters. Then we allow the user to submit values and have them processed by the template.

    Suppose we want a servlet that will print out the above message an arbitrary number of times. We can do this with the following template.

    <%args>
      int num;
    </%args>
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
    <html>
      <head>
        <title>Hello World</title>
      </head>
      <body>
    <%for int i = 0; i < num; i++ %>    Hello world!
    </%for>
        Hello world!
      </body>
    </html>
    
    (You should be able to distinguish lines that are new or changed or deleted).
    /* This Source Code Form is subject to the terms of the Mozilla Public
     * License, v. 2.0. If a copy of the MPL was not distributed with this
     * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
    package foo.bar;
    import java.io.IOException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import org.jamon.compiler.RecompilingTemplateManager;
    import org.jamon.TemplateManagerSource;
    public final class HelloServlet
        extends HttpServlet {
        public void init() {
            RecompilingTemplateManager.Data data =
                new RecompilingTemplateManager.Data();
            data.setSourceDir("templates");
            data.setWorkDir("build/work");
            TemplateManagerSource.setTemplateManager(
                new RecompilingTemplateManager(data));
        }
        public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException {
            response.setContentType("text/html");
            HelloTemplate helloTemplate = new HelloTemplate();
            helloTemplate.render(response.getWriter(), 10);
            new HelloTemplate().render(response.getWriter());
        }
        }
    
    (note that Java code identical to the previous example has been omitted.) The above code will result in "Hello World!" being printed out ten times.

    Suppose we want the user to be able to specify the number times the message is displayed. We can achieve that with a template such as:

    <%args>
      int num = 0;
      int num;
    </%args>
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
    <html>
      <head>
        <title>Hello World</title>
      </head>
      <body>
    <%for int i = 0; i < num; i++ %>    Hello world!
    </%for>
        <hr>
        <form method="GET" action="hello">
          How many times would you like to be greeted?
          <input type="text" name="num" />
          <br>
          <input type="submit" value="Greet Me"/>
        </form>
      </body>
    </html>
    
    The corresponding servlet would look like:
    /* This Source Code Form is subject to the terms of the Mozilla Public
     * License, v. 2.0. If a copy of the MPL was not distributed with this
     * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
        public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException {
            response.setContentType("text/html");
            HelloTemplate helloTemplate = new HelloTemplate();
            String numString = request.getParameter("num");
            if (numString != null) {
                helloTemplate.setNum(Integer.parseInt(numString));
            }
            helloTemplate.render(response.getWriter());
            helloTemplate.render(response.getWriter(), 10);
        }
    
    However, there is something unsatisfactory about this:
    Smell #1
    The name of the CGI parameter "num" is independently specified in two places.
    Since the object responsible for parsing the parameter seems like the natural owner of the name of the parameter, for the moment we reduce the smell by doing this:
    <%args>
      int num = 0;
    </%args>
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
    <html>
      <head>
        <title>Hello World</title>
      </head>
      <body>
    <%for int i = 0; i < num; i++ %>    Hello world!
    </%for>
        <hr>
        <form method="GET" action="hello">
          How many times would you like to be greeted?
          <input type="text" name="<% HelloServlet.NUM_KEY %>" />
          <input type="text" name="num" />
          <br>
          <input type="submit" value="Greet Me"/>
        </form>
      </body>
    </html>
    
    and
    /* This Source Code Form is subject to the terms of the Mozilla Public
     * License, v. 2.0. If a copy of the MPL was not distributed with this
     * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
        public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException {
            response.setContentType("text/html");
            HelloTemplate helloTemplate = new HelloTemplate();
            String numString = request.getParameter(NUM_KEY);
            String numString = request.getParameter("num");
            if (numString != null) {
                helloTemplate.setNum(Integer.parseInt(numString));
            }
            helloTemplate.render(response.getWriter());
        }
        public static final String NUM_KEY = "num";
    
    The next problem to address is the fact that no validation is performed on the data coming from the web. For example a user may enter "a" as the number of times they want to be greeted which would cause the doGet method to throw a NumberFormatException. Thus we need to catch this as follows (unchanged code is omitted). In the servlet we need:
    /* This Source Code Form is subject to the terms of the Mozilla Public
     * License, v. 2.0. If a copy of the MPL was not distributed with this
     * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
        public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException {
            response.setContentType("text/html");
            HelloTemplate helloTemplate = new HelloTemplate();
            String numString = request.getParameter(NUM_KEY);
            if (numString != null) {
                try {
                    helloTemplate.setNum(Integer.parseInt(numString));
                }
                catch (NumberFormatException e) {
                    helloTemplate.setError(true);
            helloTemplate.render(response.getWriter());
                }
            }
            helloTemplate.render();
        public static final String NUM_KEY = "num";
    
    and the template needs an additional error parameter:
    <%args>
      int num = 0;
      boolean error = false;
    </%args>
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
    <html>
      <head>
        <title>Hello World</title>
      </head>
      <body>
    <%for int i = 0; i < num; i++ %>    Hello world!
    </%for>
        <hr>
        <form method="GET" action="hello">
    <%if error %><b>The data you entered was invalid</b><br></%if>
          How many times would you like to be greeted?
          <input type="text" name="<% HelloServlet.NUM_KEY %>" />
          <br>
          <input type="submit" value="Greet Me"/>
        </form>
      </body>
    </html>
    
    However, in spite of all this the servlet leaves a great deal to be desired. For example invalid input should be returned to the user for editing. Furthermore there are other ways the above solution seems inadequate:
    Smell #2
    Catching errors seems ad hoc and hard to extend.
    Smell #3
    The servlet doesn't seem like the right place to be doing validation.
    The next section presents solutions to these problems.
  4. Using input elements To aid in the generation and parsing of forms, it helps to have a library of input objects and corresponding templates. For example, for text inputs, there could be an html.TextInputField class:
    /* This Source Code Form is subject to the terms of the Mozilla Public
     * License, v. 2.0. If a copy of the MPL was not distributed with this
     * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
    package foo.bar.html;
    
    public class TextInputField {
      public TextInputField(String p_name) {
        this (p_name, null);
      }
    
      public TextInputField(String p_name, String p_value) {
        m_name = p_name;
        m_value = p_value;
      }
    
      public String getName() {
        return m_name;
      }
    
      public String getValue() {
        return m_value;
      }
    
      private final String m_name;
      private final String m_value;
    }
    
    and corresponding template /html/TextInput.jamon to generate text inputs:
    <%args>
      TextInputField input;
    </%args>
    
    <input type='text' name='<% input.getName() #H %>' value='<% input.getValue() #H %>'/>
    
    Since it is the controller that will be receiving and parsing the data from the form it is natural that it decide what the name of each CGI parameter should be as well as the default value the input element should display. Hence it is natural to pass input objects like the one above into the template that renders the view. Furthermore since parsing the data that comes back also needs access to the names of the parameters it natural to place all this in one class. This is a FormFields class.

    For example, here is a FormFields class suitable for use in our "Hello World" application (you can also download a zipfile with all the sources from this section, or a WAR file ready for deployment):

    /* This Source Code Form is subject to the terms of the Mozilla Public
     * License, v. 2.0. If a copy of the MPL was not distributed with this
     * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
    package foo.bar;
    
    import java.util.Map;
    import foo.bar.html.TextInputField;
    
    public class HelloFormFields {
    
        public HelloFormFields(int p_number) {
            m_number = String.valueOf(p_number);
        }
    
        public HelloFormFields(Map p_data) {
            String[] numbers = (String[])p_data.get(NUMBER_KEY);
            if (numbers == null) {
                m_number = null;
            } else {
                m_number = numbers[0];
            }
        }
    
        public TextInputField getNumberInput() {
            return new TextInputField(NUMBER_KEY, m_number);
        }
    
        public boolean hasData() {
            return m_number != null;
        }
    
        public int parse()
            throws ValidationException {
            try {
                int number = Integer.parseInt(m_number);
                if (number < 1 || number > MAX_NUMBER) {
                    ValidationErrors errors = new ValidationErrors();
                    errors.outOfRange();
                    throw new ValidationException(errors);
                }
                return number;
            } catch (NumberFormatException e) {
                ValidationErrors errors = new ValidationErrors();
                errors.notANumber();
                throw new ValidationException(errors);
            }
        }
    
        public static class ValidationErrors {
    
            public boolean hasErrors() {
                return m_notANumber || m_outOfRange;
            }
    
            public boolean isNotANumber() {
                return m_notANumber;
            }
    
            public boolean isOutOfRange() {
                return m_outOfRange;
            }
    
            void notANumber() {
                m_notANumber = true;
            }
    
            void outOfRange() {
                m_outOfRange = true;
            }
    
            private boolean m_notANumber, m_outOfRange;
    
        } // end of inner class ValidationErrors
    
    
        public static class ValidationException
            extends Exception {
    
            ValidationException(ValidationErrors p_errors) {
                m_errors = p_errors;
            }
    
            public ValidationErrors getErrors() {
                return m_errors;
            }
    
            private final ValidationErrors m_errors;
        } // end of inner class ValidationException
    
    
        private final String m_number;
    
        private static final String NUMBER_KEY = "num";
        private static final int MAX_NUMBER = 20;
    
    }
    
    Here is a servlet that will use the above class:
    /* This Source Code Form is subject to the terms of the Mozilla Public
     * License, v. 2.0. If a copy of the MPL was not distributed with this
     * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
    package foo.bar;
    
    import java.io.IOException;
    
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.jamon.compiler.RecompilingTemplateManager;
    import org.jamon.TemplateManagerSource;
    
    public final class HelloServlet
        extends HttpServlet {
    
        public void init() {
            RecompilingTemplateManager.Data data = new RecompilingTemplateManager.Data();
            data.setSourceDir("templates");
            data.setWorkDir("build/work");
            TemplateManagerSource.setTemplateManager(
                new RecompilingTemplateManager(data));
        }
    
        public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException {
            response.setContentType("text/html");
            HelloTemplate helloTemplate = new HelloTemplate();
            HelloFormFields formFields = new HelloFormFields(request.getParameterMap());
            if (formFields.hasData()) {
                try {
                    helloTemplate.setNum(formFields.parse());
                } catch (HelloFormFields.ValidationException e) {
                    helloTemplate.setErrors(e.getErrors());
                }
            } else {
                formFields = new HelloFormFields(DEFAULT_NUMBER);
            }
            helloTemplate.render(response.getWriter(), formFields);
        }
    
        private static final int DEFAULT_NUMBER = 10;
    
    }
    
    and the corresponding template should look like:
    <%args>
      int num = 0;
      HelloFormFields formFields;
      HelloFormFields.ValidationErrors errors = new HelloFormFields.ValidationErrors();
    </%args>
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
    <html>
      <head>
        <title>Hello World</title>
      </head>
      <body>
    <%for int i = 0; i < num; i++ %>    Hello world!
    </%for>
        <hr>
        <%if errors.hasErrors() %>
          There were some problems with the data you submitted:
            <ul>
              <%if errors.isNotANumber() %><li>The data you entered is not a number</li></%if>
              <%if errors.isOutOfRange() %><li>The number you entered is out of range</li></%if>
            </ul>
        </%if>
        <form method="GET" action="hello">
          How many times would you like to be greeted?
          <& html/TextInput; input = formFields.getNumberInput() &>
          <br>
          <input type="submit" value="Greet Me"/>
        </form>
      </body>
    </html>
    
    There are a few things to note about the above example.
    • HelloFormFields produces html.TextInput objects which are rendered as HTML input elements by the /html/TextInput template
    • If the user makes an error while entering their data (for example, by typing "13q") then they will be presented with an error message together with the data they entered, so they can edit it (in this case they could just delete the "q" and resubmit).

    If you are interested in improving the above, here are some exercises:

    1. Make the error messages more fine-grained (for example, indicate when a number is negative)
    2. Indicate what the maximum allowable number is (the challenge is to find the "right" place to put the data)
    3. In the example above the same template was used to both gather user input and display the result. It is probably better to have two different templates, one dedicated to each task.
  5. Appendix

  6. Using Tomcat as your Servlet Container The reference implementation of Sun's servlet specification is Tomcat. On a Linux based system, web applications are often placed in the directory /var/tomcat4/webapps/ so you could test a WAR file called foobar.war (located in the current directory) by typing the following as superuser:
    # /sbin/service tomcat4 stop
    # cp foorbar.war /var/tomcat4/webapps
    # /sbin/service tomcat4 start
    
    (make sure that /var/tomcat4/webapps/ is writable by the user that that Tomcat runs as so that it can expand the WAR file.)

    However it is very convenient to run a servlet container as unprivilged user. This can be done directly from an Ant script by placing the following targets in your script:

      <target name="init-tomcat">
        <property name="tomcat.install.dir" value="/var/tomcat4"/>
        <property name="tomcat.home.dir" value="${basedir}/${build.dir}/tomcat"/>
        <property name="java.lib.tomcat" value="${tomcat.install.dir}/bin/bootstrap.jar"/>
        <property name="tomcat.main.class" value="org.apache.catalina.startup.Bootstrap"/>
        <property name="tmp.dir" value="${build.dir}"/>
        <mkdir dir="${tomcat.home.dir}/webapps"/>
        <mkdir dir="${tomcat.home.dir}/logs"/>
        <filter token="CONTAINER_PORT" value="${port}"/>
        <filter token="TOMCAT_HOME" value="${tomcat.home.dir}"/>
        <filter token="TOMCAT_INSTALL" value="${tomcat.install.dir}"/>
        <copy file="resources/server.xml" todir="${tomcat.home.dir}/conf" filtering="on"/>
        <copy file="resources/tomcat-users.xml" todir="${tomcat.home.dir}/conf"/>
        <copy file="resources/catalina.policy" todir="${tomcat.home.dir}/conf"/>
        <copy file="resources/default-web.xml" tofile="${tomcat.home.dir}/conf/web.xml"/>
      </target>
    
      <target name="start-tomcat"
              depends="init-tomcat"
              description="Starts the Tomcat servlet container">
        <java classname="${tomcat.main.class}"
              classpath="${java.lib.tomcat}"
              fork="yes">
          <sysproperty key="java.endorsed.dirs" value=""/>
          <sysproperty key="catalina.base" value="${tomcat.home.dir}"/>
          <sysproperty key="catalina.home" value="${tomcat.install.dir}"/>
          <sysproperty key="java.io.tmpdir" value="${tmp.dir}"/>
          <arg value="start"/>
        </java>
      </target>
    
      <target name="stop-tomcat"
              depends="init-tomcat"
              description="Stops the Tomcat servlet container">
        <java classname="${tomcat.main.class}"
              classpath="${java.lib.tomcat}"
              fork="yes">
          <sysproperty key="java.endorsed.dirs" value=""/>
          <sysproperty key="catalina.base" value="${tomcat.home.dir}"/>
          <sysproperty key="catalina.home" value="${tomcat.install.dir}"/>
          <sysproperty key="java.io.tmpdir" value="${tmp.dir}"/>
          <arg value="stop"/>
        </java>
      </target>
    
    You may need to customize the above (in particular make sure that the tomcat.install.dir is correct for your system). Also download copies of server.xml, tomcat-users.xml, catalina.policy, default-web.xml (this is distinct from the usual deployment descriptor that comes with a web application). Place them in a directory called resources. Then you can start up Tomcat by typing
    % ant start-tomcat
    and stop it by typing
    % ant stop-tomcat
    without ever needing super user privileges.
  7. Colophon This entire document was generated with the aid of Jamon. Issues such as
    • HTML escaping of templates and XML,
    • highlighting changes in snippets of Java or Jamon code, and
    • generation of source files (Java and Jamon) that exactly reflect what is is in this document
    were dealt with automatically. Furthermore, part of the process of generating this documentation was compiling some of the examples and placing the classes in WAR files. So you can be sure the examples at least compile. In particular this also means that Jamon is being used to autogenerate Jamon sources!