views:

1115

answers:

6

I am using Apache's Velocity templating engine, and I would like to create a custom Directive. That is, I want to be able to write "#doMyThing()" and have it invoke some java code I wrote in order to generate the text.

I know that I can register a custom directive by adding a line

userdirective=my.package.here.MyDirectiveName

to my velocity.properties file. And I know that I can write such a class by extending the Directive class. What I don't know is how to extend the Directive class -- some sort of documentation for the author of a new Directive. For instance I'd like to know if my getType() method return "BLOCK" or "LINE" and I'd like to know what should my setLocation() method should do?

Is there any documentation out there that is better than just "Use the source, Luke"?

A: 

Just so you know, I'm still interested, and I have not yet found anything better than "Use the source, Luke".

mcherm
Okay, and now (as of Sep 14, 2009) I am no longer trying to accomplish this. I found other approaches to solve my problem instead.
mcherm
+2  A: 

Block directives always accept a body and must end with #end when used in a template. e.g. #foreach( $i in $foo ) this has a body! #end

Line directives do not have a body or an #end. e.g. #parse( 'foo.vtl' )

You don't need to both with setLocation() at all. The parser uses that.

Any other specifics i can help with?

Also, have you considered using a "tool" approach? Even if you don't use VelocityTools to automatically make your tool available and whatnot, you can just create a tool class that does what you want, put it in the context and either have a method you call to generate content or else just have its toString() method generate the content. e.g. $tool.doMyThing() or just $myThing

Directives are best for when you need to mess with Velocity internals (access to InternalContextAdapter or actual Nodes).

Nathan Bubna
+2  A: 

Prior to velocity v1.6 I had a #blockset($v)#end directive to be able to deal with a multiline #set($v) but this function is now handled by the #define directive. Custom block directives are a pain with modern IDEs because they don't parse the structure correctly, assuming your #end associated with #userBlockDirective is an extra and paints the whole file RED. They should be avoided if possible.

I copied something similar from the velocity source code and created a "blockset" (multiline) directive.

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

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

public class BlockSetDirective extends Directive {
    private String blockKey;

    /**
     * Return name of this directive.
     */
    public String getName() {
        return "blockset";
    }

    /**
     * Return type of this directive.
     */
    public int getType() {
        return BLOCK;
    }

    /**
     * simple init - get the blockKey
     */
    public void init( RuntimeServices rs, InternalContextAdapter context,
                      Node node )
        throws TemplateInitException {
        super.init( rs, context, node );
        /*
         * first token is the name of the block. I don't even check the format,
         * just assume it looks like this: $block_name. Should check if it has
         * a '$' or not like macros.
         */
        blockKey = node.jjtGetChild( 0 ).getFirstToken().image.substring( 1 );
    }

    /**
     * Renders node to internal string writer and stores in the context at the
     * specified context variable
     */
    public boolean render( InternalContextAdapter context, Writer writer,
                           Node node )
        throws IOException, MethodInvocationException,
        ResourceNotFoundException, ParseErrorException {
        StringWriter sw = new StringWriter(256);
        boolean b = node.jjtGetChild( 1 ).render( context, sw );
        context.put( blockKey, sw.toString() );
        return b;
    }

}
gbegley
+2  A: 

Also was trying to come up with a custom directive. Couldn't find any documentation at all, so I looked at some user created directives: IfNullDirective (nice and easy one), MergeDirective as well as velocity build-in directives.

Here is my simple block directive that returns compressed content (complete project with some directive installation instructions is located here):

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.parser.node.Node;
import org.apache.velocity.runtime.log.Log;

import com.googlecode.htmlcompressor.compressor.HtmlCompressor;

/**
 * Velocity directive that compresses an HTML content within #compressHtml ... #end block.
 */
public class HtmlCompressorDirective extends Directive {

    private static final HtmlCompressor htmlCompressor = new HtmlCompressor();

    private Log log;

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

    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();

     //set compressor properties
     htmlCompressor.setEnabled(rs.getBoolean("userdirective.compressHtml.enabled", true));
     htmlCompressor.setRemoveComments(rs.getBoolean("userdirective.compressHtml.removeComments", true));
    }

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

        //render content to a variable
        StringWriter content = new StringWriter();
     node.jjtGetChild(0).render(context, content);

     //compress
     try {
      writer.write(htmlCompressor.compress(content.toString()));
     } catch (Exception e) {
      writer.write(content.toString());
      String msg = "Failed to compress content: "+content.toString();
            log.error(msg, e);
            throw new RuntimeException(msg, e);

     }
     return true;

    }

}
serg
+2  A: 

On the Velocity wiki, there's a presentation and sample code from a talk I gave "Hacking Velocity". It includes an example of a custom directive.

http://wiki.apache.org/jakarta-velocity/HackingVelocity

Will Glass
+4  A: 

I put together a little article about writing custom velocity directives (and tools). Maybe someone will find it useful.

serg