X
Business

Templating mechanism simplifies scripting work

Conditional templates help users by dynamically determining if certain parts of the template should be applied.
Written by Lee Chuk-Munn, Contributor

In a recent project that I was working on, I found myself having to generate the following String0 programatically.


var ___0AFlamer = new terminator();

___0AFlamer.type = FLAMER;

___0AFlamer.event["dead"] = "self.dead = true; endgame();";

___0AFlamer.event["end_move"] = "if (self.ammunition == 0) endgame();";
AFlamer = function(c) {
list = new LinkedList();
var idx;
if ((arguments.length <= 0) || (typeof c != 'number'))
idx = 1;
else
idx = c;
for (var i = 0; i < idx; i++) {

___0AFlamer.makeName('AFlamer');
list.add(
___0AFlamer.clone());
}
return (list);
}

The parts in red was the name of the object. The lines in greenvaries depending on what the events and properties that were defined on the object.

The "brute force" way
My first cut of this solution was a combination of String concatenation and clever, or so I thought, pattern replacement with regular expression.

Here is how it looks...I first used a StringBuilder to create the first part of the String:

StringBuilder strBuilder = new StringBuilder();
String intName = "___0" + name;
...
strBuilder.append("var ").append(intName).append(" = new terminator();\n");
strBuilder.append(intName).append(".type = ").append(type).append(";\n");
...
while (!stop) {
    ...
    switch (k) {
        case property:
            str = head(shift(currLine, 1).trim());
            strBuilder.append(intName).append(".property[\"").append(str)
                    .append("\"] = ").append("\"").append(shift(currLine, 2)).append("\";\n");
            break;
        ...
    }
}

Once I've established the top portion, the remainder of the String is fairly static. So I resorted to defining a String constant with a series of markers, XXXX and YYYY, which I would use as regular expression to locate and replace.

Here is how it looks:

private static final String PROTOTYPE_TERMINATOR = " = function(c) {\nlist = new LinkedList();\n"
        + "var idx;\nif ((arguments.length <= 0) || (typeof c != 'number'))\nidx = 1;\n"
        +  "else\nidx = c;\nfor (var i = 0; i < idx; i++) {\nXXXX.makeName('YYYY');\nlist.add(XXXX.clone());\n}\nreturn (list);\n}\n";
...
String prot = PROTOTYPE_TERMINATOR.replaceAll("
XXXX", intName);
prot = prot.replaceAll("
YYYY", name);
strBuilder.append(name).append(prot);  

I was quite proud of what I've done until I ran it for the first time. The String that was generated was incorrect. There were missing quotes, commas, line breaks, etc. I found it really difficult to visualize what was generated.

The second problem was when I had to change what was generated. This meant I had to change my code which resulted in another round of hunting down missing quotes, commas and line breaks.

String template--first try
The StringTemplate templating engine is a tool for generating formatted text. Here is my first attempt with StringTemplate:

private static final String PROTOTYPE_TERMINATOR = "$name$ = function(c) {\nlist = new LinkedList();\n"
        + "var idx;\nif ((arguments.length <= 0) || (typeof c != 'number'))\nidx = 1;\n"
        + "else\nidx = c;\nfor (var i = 0; i < idx; i++) {\n$prototype$.makeName('$name$');\nlist.add(
$prototype$.clone());\n}\nreturn (list);\n}\n";

StringTemplate t = new StringTemplate(PROTOTYPE_TERMINATOR);
t.setAttribute("prototype", "___0AFlamer");
t.setAttribute("name", "AFlamer");
System.out.println(t.toString());

As you can see StringTemplate allows us to create a parameterized String where the values that are to be interpolated are annotated with a pair of $. In the above example, the prototype and name are attributes. After we have created an instance of StringTemplate, we call setAttribute to replace the parameters with the actual value before dumping out the result.

This approach of using StringTemplate is no different than using my previous method where I use regular expression for String replacement. Furthermore it has not solve the 2 problems that I faced in.

String template--first attempt
One feature of StringTemplate is to allow you to define a template outside your code. You can then subsequently load this template into your application.

This solves two problems; first your template is now independent of your code. And if you did not introduce any new attributes into your template, you do not have to recompile your code1. The second problem that this resolves is that it removes all that nasty escapes (backslashs).

A template is just a text file that ends with the '.st' suffix. My template will be called prototype.st and this will be stored in a directory called templates.

Here is how prototype.st looks like:

var $prototype$ = new terminator();
$prototype$.type = $type$;

$name$ = function(c) {
    list = new LinkedList();
    var idx;
    if ((arguments.length <= 0) || (typeof c != 'number'))
        idx = 1;
    else
        idx = c;
    for (var i = 0; i < idx; i++) {
        $prototype$.makeName('$name$');
        list.add($prototype$.clone());
    }
    return (list);
}

Now to access prototype.st and use it to generate our String, we use StringTemplateGroup to load it. What we need to tell StringTemplateGroup the location of our templates. The following code snippet shows how we create a StringTemplateGroup, and use it to load prototype.st.

The relevant parts of the code are shown in blue.

// ./templates is the location of the templates, terminator is an arbitrary name
StringTemplateGroup templates = new StringTemplateGroup("terminator", "./templates");
// Look for the prototype.st template
StringTemplate t = templates.getInstanceOf("prototype");
// As before
t.setAttribute("prototype", "___0AFlamer");
t.setAttribute("name", "AFlamer");
t.setAttribute("type", "FLAMER");
System.out.println(t.toString());

Conditions, Loops and Includes
One of the requirement of String that I am generating is that there is a part which specifies events. The number of events varies according a user's configuration file.

Every event has a name and some JavaScript codes associated with that event. These events are held in an object called EventBeanEventBean has 2 JavaBean properties called name and code which as we shall see, StringTemplate can use.

public class EventBean {

    private String name;
    private String code;

    public EventBean(String n, String c) {
        name = n;
        code = c;
    }
    public String getName() {
        return (name);
    }
    public String getCode() {
        return (code);
    }
}

An array of EventBean will be created and passes as an attribute to StringTemplate. We will use a loop to iterate through the array and generate the text accordingly. Here is the modified prototype.st.

var $prototype$ = new terminator();
$prototype$.type = $type$;

$if (hasEvents)$
$events:{ e | $event(ev=e)$ }$
$endif$

$name$ = function(c) {


    list = new LinkedList();
    var idx;
    if ((arguments.length <= 0) || (typeof c != 'number'))
        idx = 1;
    else
        idx = c;
    for (var i = 0; i < idx; i++) {
        $prototype$.makeName('$name$');
        list.add($prototype$.clone());
    }
    return (list);
}

In the above StringTemplate we see conditional templates, loops and template includes. These allow us to control dynamically how a template is generated.

A conditional template provides an if-like statement. This allows you to dynamically determine if certain parts of the template should be applied.

The boolean expression, hasEvents above, is set as a boolean attribute.
If an attribute in a template is an array, we can create a sub-template and iteratively apply the value to the sub-template.

The-sub template in the above example is shown in green. The e is the control variable.

Finally, the $event()$ is a template call event.st to be included in the processing of this template. event.st is also stored in templates directory.

The following shows an  event.st template:

$prototype$.event["$ev.name$"] = "$ev.code$";

On every iteration of the loop we invoke event.st  by binding e to event.st's ev attribute. Since e (or ev) is an EventBean object, we can invoke its getter by just accessing the name of the property.  

After we have completed evaluation of event.st, the result will be inserted into prototype.st.

What is really nice about template includes is that every template that we write can potentially be reused in another template.

>The following piece of Java code sets up the various StringTemplate attributes.
EventVO[] eventList = new EventVO[2];
// Populate eventList
...
StringTemplateGroup templates = new StringTemplateGroup("terminator", "./templates");
StringTemplate t = templates.getInstanceOf("prototype");
t.setAttribute("prototype", "___0AFlamer");
t.setAttribute("name", "AFlamer");
t.setAttribute("type", "FLAMER");
t.setAttribute("hasEvents", true);
t.setAttribute("events", eventList);
System.out.println(t.toString());

There are other templating libraries like Apache Velocity and Freemarker which have JSR-223 (Scripting) integration. Both of these provide about the same capabilities as StringTemplate.
So if you find yourself performing a lot of String concatenation and contorting your String with cleverly placed backslashes, maybe its time to look at a templating mechanism.

Lee Chuk-Munn is a staff engineer at Sun Microsystems.


0 What I am attempting to do here is to generate a JavaScript object based on some input from a configuration file
1 If you use a property file to store your attribute-value pair, you can avoid recompiling




Editorial standards