Creating Custom Directives for Apache Velocity

Java Velocity

In this article we will go over Apache Velocity API and write our own inline and block directives. Like in the previous article about creating Velocity tools, we will be trying to duplicate the behavior of truncate() method from DisplayTools which truncates a long string.

There are two directive types in Velocity - inline and block directives. Inline directives consist from a single line, while block directives have body and closing tag #end:

#include( "one.gif","two.txt","three.htm" )
#if( $foo )
   Block directive...
#end

Creating inline directive

To declare your custom directive you need to specify userdirective parameter in velocity config (for example in velocity.properties file) and provide comma separated list of complete user directive class names:

userdirective=com.example.MyDirective1, com.example.MyDirective2

Lets create our first inline directive #truncate with 4 parameters:

#truncate(Object truncateMe, int maxLength, String suffix, boolean truncateAtWord) 

First parameter contains a string we want to truncate and is the only required parameter, the other 3 set optional truncating settings.

Lets look at the final directive code and then go over it in details:

package ca.sergiy.velocity;

import java.io.IOException;
import java.io.Writer;

import org.apache.velocity.context.InternalContextAdapter;
import org.apache.velocity.exception.MethodInvocationException;
import org.apache.velocity.exception.ParseErrorException;
import org.apache.velocity.exception.ResourceNotFoundException;
import org.apache.velocity.runtime.directive.Directive;
import org.apache.velocity.runtime.parser.node.Node;

public class TruncateDirective extends Directive {

    public String getName() {
        return "truncate";
    }

    public int getType() {
        return LINE;
    }

    public boolean render(InternalContextAdapter context, Writer writer, Node node) 
            throws IOException, ResourceNotFoundException, ParseErrorException, MethodInvocationException {

        //setting default params
        String truncateMe = null;
        int maxLength = 10;
        String suffix = null;
        Boolean truncateAtWord = false;

        //reading params
        if (node.jjtGetChild(0) != null) {
            truncateMe = String.valueOf(node.jjtGetChild(0).value(context));
        }

        if (node.jjtGetChild(1) != null) {
            maxLength = (Integer)node.jjtGetChild(1).value(context);
        }

        if (node.jjtGetChild(2) != null) {
            suffix = String.valueOf(node.jjtGetChild(2).value(context));
        }

        if (node.jjtGetChild(3) != null) {
            truncateAtWord = (Boolean)node.jjtGetChild(3).value(context);
        }

        //truncate and write result to writer
        writer.write(truncate(truncateMe, maxLength, suffix, truncateAtWord));

        return true;

    }

    //does actual truncating (taken directly from DisplayTools)
    public String truncate(String truncateMe, int maxLength, String suffix, boolean truncateAtWord) {
        if (truncateMe == null || maxLength <= 0) {
            return null;
        }

        if (truncateMe.length() <= maxLength) {
            return truncateMe;
        }
        if (suffix == null || maxLength - suffix.length() <= 0) {
            // either no need or no room for suffix
            return truncateMe.substring(0, maxLength);
        }
        if (truncateAtWord) {
            // find the latest space within maxLength
            int lastSpace = truncateMe.substring(0, maxLength - suffix.length() + 1).lastIndexOf(" ");
            if (lastSpace > suffix.length()) {
                return truncateMe.substring(0, lastSpace) + suffix;
            }
        }
        // truncate to exact character and append suffix
        return truncateMe.substring(0, maxLength - suffix.length()) + suffix;

    }

}

All directives in Apache extend Directive class that has 3 abstract methods we need to implement: getName(), getType() and render().

First method getName() should return a name of your directive that will be used in templates.

Method getType() returns BLOCK or LINE constants which determine a directive type.

Third method render(InternalContextAdapter context, Writer writer, Node node) is where all the work is happening. Writer is our template writer where we are going to write the result. Node object contains information about our directive (its parameters and properties) and InternalContextAdapter contains everything Velocity needs to know about the template in order to render it.

Directive parameters (or, more precisely, nodes, representing parameters) can be accessed from directive node by calling node.jjtGetChild(i), where i is parameter number (starting from zero). You can get total number of parameters using node.jjtGetNumChildren().

jjtGetChild(i) method returns Node object, from which you can either get "rendered" value by calling node.jjtGetChild(i).value() method, or retrieve its literal value by calling node.jjtGetChild(i).literal(). To demonstrate the difference between rendered and literal values lets take a look at this template:

#set($test = "Test Value")
#truncate("This is $test")

(String)node.jjtGetChild(0).value() will return "This is Test Value", while (String)node.jjtGetChild(0).literal() will return "This is $test". In most cases you will need rendered values.

Lets test our newly created directive. First we need to declare it in config file so Velocity could find it:

userdirective=ca.sergiy.velocity.TruncateDirective

Now lets run this template:

#set($test = "long line that should be truncated")
#truncate("Testing $test", 20, "...", true)

Result:

Testing long line... 

Creating block directive

Now lets turn our inline directive into block directive:

#truncateBlock(int maxLength, String suffix, boolean truncateAtWord) 
    Long block 
    that will be 
    truncated
#end

It will work the same as before with all parameters being optional. Lets look at the code:

package ca.sergiy.velocity;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;

import org.apache.velocity.context.InternalContextAdapter;
import org.apache.velocity.exception.MethodInvocationException;
import org.apache.velocity.exception.ParseErrorException;
import org.apache.velocity.exception.ResourceNotFoundException;
import org.apache.velocity.exception.TemplateInitException;
import org.apache.velocity.runtime.RuntimeServices;
import org.apache.velocity.runtime.directive.Directive;
import org.apache.velocity.runtime.log.Log;
import org.apache.velocity.runtime.parser.node.ASTBlock;
import org.apache.velocity.runtime.parser.node.Node;

public class TruncateBlockDirective extends Directive {

    private Log log;

    private int maxLength;
    private String suffix;
    private Boolean truncateAtWord;

    public String getName() {
        return "truncateBlock";
    }

    public int getType() {
        return BLOCK;
    }

    @Override
    public void init(RuntimeServices rs, InternalContextAdapter context, Node node) throws TemplateInitException {
        super.init(rs, context, node);
        log = rs.getLog();

        //read dafault values from config
        maxLength = rs.getInt("userdirective.truncateBlock.maxLength", 10);
        suffix = rs.getString("userdirective.truncateBlock.suffix", "...");
        truncateAtWord = rs.getBoolean("userdirective.truncateBlock.truncateAtWord", false);

    }

    public boolean render(InternalContextAdapter context, Writer writer, Node node) 
            throws IOException, ResourceNotFoundException, ParseErrorException, MethodInvocationException {

        log.debug("truncateBlock directive render() call");

        String truncateMe = null;

        //default settings
        int maxLength = this.maxLength;
        String suffix = this.suffix;
        Boolean truncateAtWord = this.truncateAtWord;

        //loop through all "params"
        for(int i=0; i<node.jjtGetNumChildren(); i++) {
            if (node.jjtGetChild(i) != null ) {
                if(!(node.jjtGetChild(i) instanceof ASTBlock)) {
                    //reading and casting inline parameters
                    if(i == 0) {
                        maxLength = (Integer)node.jjtGetChild(i).value(context);
                    } else if(i == 1) {
                        suffix = String.valueOf(node.jjtGetChild(i).value(context));
                    } else if(i == 2) {
                        truncateAtWord = (Boolean)node.jjtGetChild(i).value(context);
                    } else {
                        break;
                    }
                } else {
                    //reading block content and rendering it  
                    StringWriter blockContent = new StringWriter();
                    node.jjtGetChild(i).render(context, blockContent);

                    truncateMe = blockContent.toString();
                    break;
                }
            }
        }

        //truncate and write result to writer
        try {
            writer.write(truncate(truncateMe, maxLength, suffix, truncateAtWord));
        } catch (Exception e) {
            String msg = "Truncate failed";
            log.error(msg, e);
            throw new RuntimeException(msg, e);

        }
        return true;

    }

    //does actual truncating (taken directly from DisplayTools)
    public String truncate(String truncateMe, int maxLength, String suffix,
            boolean truncateAtWord) {
        if (truncateMe == null || maxLength <= 0) {
            return null;
        }

        if (truncateMe.length() <= maxLength) {
            return truncateMe;
        }
        if (suffix == null || maxLength - suffix.length() <= 0) {
            // either no need or no room for suffix
            return truncateMe.substring(0, maxLength);
        }
        if (truncateAtWord) {
            // find the latest space within maxLength
            int lastSpace = truncateMe.substring(0, maxLength - suffix.length() + 1).lastIndexOf(" ");
            if (lastSpace > suffix.length()) {
                return truncateMe.substring(0, lastSpace) + suffix;
            }
        }
        // truncate to exact character and append suffix
        return truncateMe.substring(0, maxLength - suffix.length()) + suffix;

    }

}

First difference is that we additionally overrode init() method from Directive class that allows us to get access to RuntimeServices object in order to get logger instance and read default directive parameters from configuration file. So now our velocity.properties file looks like this:

userdirective=ca.sergiy.velocity.TruncateDirective, ca.sergiy.velocity.TruncateBlockDirective
userdirective.truncateBlock.maxLength=10
userdirective.truncateBlock.suffix=...
userdirective.truncateBlock.truncateAtWord=false

Setting all default parameter values in config is optional, because if parameter is not present we will still assign hardcoded default value. For example this line sets maxLength to userdirective.truncateBlock.maxLength value if present, otherwise it is set to 10:

maxLength = rs.getInt("userdirective.truncateBlock.maxLength", 10);

There is no official recommendations for config parameter naming for user directives, but using userdirective.{directive}.{param} format should be good choice.

render() method looks a little different this time as well. The reason for this is that block content is passed as node's last child (after other inline parameters), but because all our parameters are optional we need to somehow separate block content from other inline parameters. One way of doing this is to check chlid node's class name. For block content it is always ASTBlock, which is different from the rest. In order to render ASTBlock value we have to use render(InternalContextAdapter context, Writer writer) method that will render it to a provided writer (as opposing to simply calling node.value() for inline parameters).

Conclusion

In this article we looked at creating custom user directives for Apache Velocity. But before writing your own Velocity directive you should consider using tools approach instead if possible, as writing custom directives should be reserved for extending core template language or working with multiline parameters. For example if "truncate" method was not present in DispalyTools already, proper way of doing it would be creating new Tool class and adding it there instead of making it a directive.

Creating custom directives is not too complicated, but they are not well documented and it is hard to find any info or tutorials about them. If you need more examples take a look at Velocity built-in directive sources. There are some simple directive examples on VelocityTools wiki. You can also check out my open-source htmlcompressor project that has some block directives.

Sep 9, 2009
profile for serg at Stack Overflow, Q&A for professional and enthusiast programmers