Skip to main content

Trivial Templating

Posted by evanx on July 9, 2010 at 10:20 AM PDT

Notwithstanding the fact that anyone in their right mind (which rules me out) would use Apache Velocity or FreeMarker for templating, we present a trivial templating helper class, where for instance we have an HTML template as follows to send a confirmation email to a customer.

<p>Hey ${displayName}</span>
<p>Your Travelstart reference: <span class="bookingReference">${bookingReference}</span>
<table class="travelerTable">
    <tbody>
        <tr class="travelerRow">
            <td>${travelerName}</td>
        </tr>
    </tbody>
</table>

where the above is used to compose an HTML email, and/or render a PDF document using FlyingSaucer.

So we invoke the "templating engine" as follows.

<b>public</b> String generate(String resourceName) {
    HtmlTemplate template = <b>new</b> HtmlTemplate(getClass().getResourceAsStream(resourceName));
    template.setProperty(&quot;displayName&quot;, &quot;Evan&quot;);
    template.setProperty(&quot;bookingReference&quot;, &quot;555555&quot;);
    template.setProperty(&quot;bookingDate&quot;, &quot;16 Jul 2010&quot;);
    template.setProperty(&quot;amount&quot;, &quot;R 1,200.00&quot;);
    template.setProperty(&quot;travelerRow&quot;, 0, &quot;travelerName&quot;, &quot;Evan Summers&quot;);
    template.setProperty(&quot;travelerRow&quot;, 1, &quot;travelerName&quot;, &quot;Tootie Milburn&quot;);
    <b>return</b> template.compose();
}

This uses the trivial templating class below to read the HTML template, and substitute the given values into the template, etc.

<b>public</b> <b>class</b> HtmlTemplate {
    List&lt;String&gt; lineList = <b>new</b> ArrayList();
    InputStream inputStream;
    StringBuilder templateBuilder = <b>new</b> StringBuilder();
    String trClass = <b>null</b>;
    StringBuilder rowBuilder = <b>new</b> StringBuilder();
    String line;
    EntryList entryList = <b>new</b> EntryList();

    <b>public</b> HtmlTemplate(InputStream inputStream) {
        <b>this</b>.inputStream = inputStream;
    }

    <b>public</b> <b>void</b> setProperty(String name, Object value) {
        entryList.getList().add(new Entry(<b>null</b>, 0, name, value));
    }

    <b>public</b> <b>void</b> setProperty(String parent, <b>int</b> index, String name, Object value) {
        entryList.getList().add(new Entry(parent, index, name, value));
    }
    ...
}

Once the properties to substitute have been given using setProperty() above, we invoke compose() below, which handles multiple rows by looking for <tr> and </tr> elements with the appropriate CSS class.

    <b>public</b> String compose() <b>throws</b> Exception {
        BufferedReader reader = <b>new</b> BufferedReader(new InputStreamReader(inputStream));
        <b>while</b> (true) {
            line = reader.readLine();
            <b>if</b> (line == <b>null</b>) {
                <b>return</b> templateBuilder.toString();
            }
            line += &quot;\n&quot;;
            boolean trClosed = line.trim().equals(&quot;&lt;/tr&gt;&quot;);
            boolean tableClosed = line.trim().equals(&quot;&lt;/table&gt;&quot;);
            <b>if</b> (trClass != <b>null</b>) {
                <b>if</b> (trClosed) {
                    rowBuilder.append(line);
                    templateBuilder.append(replaceRow());
                    rowBuilder.setLength(0);
                    line = <b>null</b>;
                } <b>else</b> <b>if</b> (tableClosed) {
                    trClass = <b>null</b>;
                } <b>else</b> <b>if</b> (rowBuilder.length() &gt; 0) {
                    rowBuilder.append(line);
                    line = <b>null</b>;
                }
            } <b>else</b> {
                trClass = getRow();
                <b>if</b> (trClass != <b>null</b>) {
                    rowBuilder.setLength(0);
                    rowBuilder.append(line);
                    line = <b>null</b>;
                }
            }
            <b>if</b> (line != <b>null</b>) {
                templateBuilder.append(replace());
            }
        }
    }

which relies on the following methods to do the donkey-work.

    <b>protected</b> String getRow() {
        <b>for</b> (String name : entryList.getParentList()) {
            <b>if</b> (line.indexOf(&quot;&lt;tr class=\&quot;&quot; + name) &gt;= 0) {
                <b>return</b> name;
            }
        }
        <b>return</b> <b>null</b>;
    }

    <b>protected</b> String replaceRow() {
        <b>int</b> size = entryList.getListSize(trClass);
        StringBuilder builder = <b>new</b> StringBuilder();
        <b>for</b> (<b>int</b> rowIndex = 0; rowIndex &lt; size; rowIndex++) {
            builder.append(replaceRow(rowIndex));
        }
        <b>return</b> builder.toString();
    }

    <b>protected</b> String replaceRow(<b>int</b> rowIndex) {
        StringBuilder builder = <b>new</b> StringBuilder(rowBuilder);
        Map&lt;String, Object&gt; rowMap = entryList.getMap(trClass, rowIndex);
        <b>for</b> (String name : rowMap.keySet()) {
            Object value = rowMap.get(name);
            String pattern = &quot;${&quot; + name + &quot;}&quot;;
            <b>int</b> index = builder.indexOf(pattern);
            <b>if</b> (index &gt; 0) {
                builder.replace(index, index + pattern.length(), value.toString());
            }
        }
        <b>return</b> builder.toString();
    }

    <b>protected</b> String replace() {
        Map&lt;String, Object&gt; valueMap = entryList.getMap(0);
        <b>for</b> (String name : valueMap.keySet()) {
            Object value = valueMap.get(name);
            String pattern = &quot;${&quot; + name + &quot;}&quot;;
            <b>int</b> index = line.indexOf(pattern);
            <b>if</b> (index &gt; 0) {
                <b>return</b> line.substring(0, index) + value + line.substring(index + pattern.length());
            }
        }
        <b>return</b> line;
    }

We use the following rough-shod classes to coddle the data to substitute into the template.

<b>class</b> EntryList {
    List&lt;Entry&gt; list = <b>new</b> ArrayList();

    <b>public</b> List&lt;Entry&gt; getList() {
        <b>return</b> list;
    }

    <b>public</b> <b>int</b> getListSize(String parent) {
        <b>int</b> maxIndex = 0;
        <b>for</b> (Entry entry : list) {
            <b>if</b> (entry.parent != <b>null</b> &amp;&amp; entry.parent.equals(parent)) {
                <b>if</b> (entry.index &gt; maxIndex) {
                    maxIndex = entry.index;
                }
            }
        }
        <b>return</b> maxIndex + 1;
    }

    <b>public</b> Map&lt;String, Object&gt; getMap(String parent, <b>int</b> index) {
        Map&lt;String, Object&gt; map = <b>new</b> HashMap();
        <b>for</b> (Entry entry : list) {
            <b>if</b> (entry.parent != <b>null</b> &amp;&amp; entry.parent.equals(parent) &amp;&amp; entry.index == index) {
                map.put(entry.name, entry.value);
            }
        }
        <b>return</b> map;
    }

    <b>public</b> Map&lt;String, Object&gt; getMap(<b>int</b> index) {
        Map&lt;String, Object&gt; map = <b>new</b> HashMap();
        <b>for</b> (Entry entry : list) {
            <b>if</b> (entry.parent == <b>null</b> &amp;&amp; entry.index == index) {
                map.put(entry.name, entry.value);
            }
        }
        <b>return</b> map;
    }

    <b>public</b> List&lt;String&gt; getParentList() {
        List&lt;String&gt; parentList = <b>new</b> ArrayList();
        <b>for</b> (Entry entry : list) {
            <b>if</b> (entry.parent != <b>null</b> &amp;&amp; !parentList.contains(entry.parent)) {
                parentList.add(entry.parent);
            }
        }
        <b>return</b> parentList;
    }
}

where the above handles a list of the following tuples, allowing for a parent and index for rows.

<b>class</b> Entry {
    String parent;
    <b>int</b> index;
    String name;
    Object value;

    <b>public</b> Entry(String parent, <b>int</b> index, String name, Object value) {
        <b>this</b>.parent = parent;
        <b>this</b>.index = index;
        <b>this</b>.name = name;
        <b>this</b>.value = value;
    }
}

But of course one should keep it real with Apache Velocity or FreeMarker. I was just decided to have some trivial coding fun :)

Related Topics >>