9.11 Creating an XDoclet Tag Handler

9.11.1 Problem

You want to create a new XDoclet Tag Handler.

9.11.2 Solution

Extend the xdoclet.XDocletTagSupport class; write public methods that perform logic and generate content. The methods are referenced in an XDoclet template file (.xdt).

9.11.3 Discussion

The previous recipe created a custom Ant XDoclet subtask, providing an entry point into our custom code generator. Now it is time to write the tag handler class, which is responsible for generating snippets of content and performing simple logic.

There are no methods that must be overridden or directly implemented when creating a custom tag handler. Rather, you create public methods that a template file references.

Let's see how to write the tag handler class JUnitPerfTagHandler:

  1. Create a new Java source file called JUnitPerfTagHandler.java and add it to your project.

  2. Add the following imports:

    import xdoclet.XDocletException;
    import xdoclet.XDocletTagSupport;
    import xdoclet.tagshandler.TypeTagsHandler;
    import java.text.MessageFormat;
  3. Extend the XDocletTagSupport class:

    public class JUnitPerfTagHandler extends XDocletTagSupport {
  4. Add public methods to generate snippets of content and to perform logic.

Step three deserves further explanation. There are two categories of methods used in a tag handler class: block and content.

9.11.3.1 Block

Block methods are used for iterating and performing logic, which is synonymous with for loops and if statements. A block method accepts a single parameter containing any content that should be parsed if a condition is met. The generate(String) method is provided by the base class and used to continue processing nested content, if necessary.

The code snippet below shows how to check if the current class being evaluated is an instance of junit.framework.TestCase. This method shows an example usage of the XDoclet utility class TypeTagsHandler.

public void ifIsTestCase(String template) throws XDocletException {
    if (TypeTagsHandler.isOfType(getCurrentClass(  ),
                                 "junit.framework.TestCase",
                                 TypeTagsHandler.TYPE_HIERARCHY)) {
        generate(template);
    }
}

This method is never referenced in the junitperf.xdt file; rather, it is shown here for illustration purposes.

9.11.3.2 Content

Content tags are used for outputting information. These tags are synonymous with getter methods that return a string. Content tags never contain nested information. The snippet below shows how to generate the JUnitPerf test classname.

public String className(  ) throws XDocletException {
    JUnitPerfDocletSubTask task = (JUnitPerfDocletSubTask)
            getDocletContext().getActiveSubTask(  );

    String currentJUnitTest = getCurrentClass().getName(  );
    return MessageFormat.format(task.getJUnitPerfPattern(  ),
                                new Object[] { currentJUnitTest });
}

The example above has a few interesting details that deserve some attention. First, the call to getDocletContext( ) returns an instance of an xdoclet.DocletContext class. A DocletContext object contains information passed to the Ant XDoclet task in the buildfile. For example, the DocletContext object contains the destination directory, whether or not the `force' attribute is set, the active subtask,[7] and numerous other attributes. Here we retrieve the active subtask, which we know must be a JUnitPerfDocletSubTask because it is the only subtask we created. Next, we retrieve the name of the class that is currently being parsed by XDoclet, which should be the classname of a JUnit test. Once we have the current JUnit classname we need to generate the JUnitPerf test class name. This is done using the java.text.MessageFormat class to perform a text substitution between the classname pattern and the classname of the JUnit test. The classname pattern is retrieved using the getJUnitPerfPattern( ) method on the subtask. Recall that this is the value of the "destinationFile" attribute set in the Ant buildfile, but without the filename extension. Let's take a look at a simple example. Suppose the JUnitPerf pattern is "TestPerf{0}", and the name of the current JUnit test class name is "TestExample". The output of MessageFormat.format( ), using these values as parameters, yields "TestPerfTestExample".

[7] An active subtask is the Ant subtask currently being processed. Remember that an Ant task may have multiple subtasks.

Example 9-12 shows the full implementation of the JUnitPerfTagHandler class.

Example 9-12. JUnitPerfTagHandler
package com.oreilly.javaxp.xdoclet.perf;

import xdoclet.XDocletException;
import xdoclet.XDocletTagSupport;
import xdoclet.tagshandler.TypeTagsHandler;

import java.text.MessageFormat;

/**
 * Provides a window into the junitperf.xdt template file. The instance
 * methods can be used in the template file to retrieve or perform
 * logic. See the junitperf.xdt file for how these methods are referenced.
 */
public class JUnitPerfTagHandler extends XDocletTagSupport {

    public static final String TIMED_TEST = "junitperf.timedtest";
    public static final String LOAD_TEST = "junitperf.loadtest";
    public static final String WAIT_FOR_COMPLETION = "waitForCompletion";
    public static final String NUMBER_OF_USERS = "numberOfUsers";
    public static final String NUMBER_OF_ITERATIONS = "numberOfIterations";
    public static final String MAX_ELAPSED_TIME = "maxElapsedTime";

    /**
     * If the current class being evaluated extends <code>TestCase</code>
     * then process the data contained in the given parameter.
     *
     * @param template the current block of text to be parsed.
     */
    public void ifIsTestCase(String template) throws XDocletException {
        if (TypeTagsHandler.isOfType(getCurrentClass(  ),
                                     "junit.framework.TestCase",
                                     TypeTagsHandler.TYPE_HIERARCHY)) {
            generate(template);
        }
    }

    /**
     * This shows an example of a content-level method that returns
     * the name of the generated class. See the junitperf.xdt file for 
     * an example of how to use this method.
     *
     * <p>
     * This method shows how to use the active Ant subtask to retrieve
     * attributes defined in the Ant buildfile. Here we extract out the
     * 'jUnitPerfPattern' attribute.
     * </p>
     *
     * @return the new name of the generated class.
     */
    public String className(  ) throws XDocletException {
        JUnitPerfDocletSubTask task = (JUnitPerfDocletSubTask)
                getDocletContext().getActiveSubTask(  );

        String currentJUnitTest = getCurrentClass().getName(  );
        return MessageFormat.format(task.getJUnitPerfPattern(  ),
                                    new Object[]{currentJUnitTest});
    }

    /**
     * This method shows an example of how to use XDoclet to output
     * code that instantiates an object.
     *
     * @return a line of code that instantiates a new JUnitPerf <code>
     * TimedTest</code>.
     */
    public String timedTest(  ) throws XDocletException {
        return "new TimedTest(" + getJUnitConstructor(  ) +
                ", " + getMaxElapsedTime(  ) +
                ", " + getWaitForCompletion(  ) + ");";
    }

    /**
     * This method shows an example of how to use XDoclet to output
     * code that instantiates an object.
     *
     * @return a line of code that instantiates a new JUnitPerf <code>
     * LoadTest</code>.
     */
    public String loadTest(  ) throws XDocletException {
        return "new LoadTest(" + getJUnitConstructor(  ) +
                ", " + getNumberOfUsers(  ) +
                ", " + getNumberOfIterations(  ) + ");";
    }

    /**
     * Helper method that retrieves the current class being evaluated. This
     * class should be an instance of a JUnit <code>TestCase</code> and
     * therefore we pass a single parameter, which is the current method name
     * being evaluated. This produces a new instance of a <code>TestCase</code>
     * that executes a single JUnit test method. For example:
     * <code>new TestExample("testExampleLoad");</code> might be
     * the output of the method. 
     *
     * @return a line of code that constructs a new <code>TestCase</code>
     * method for executing a single JUnit test method.
     */
    private String getJUnitConstructor(  ) {
        return "new " + getCurrentClass().getName(  ) + "(\"" +
                getCurrentMethod().getName(  ) + "\")";
    }

    /**
     * Helper method that retrieves the number of users to use for a
     * <code>LoadTest</code>.
     *
     * @return the number of users to use for a load test. This is a mandatory
     * XDoclet tag parameter (attribute).
     * @throws XDocletException if this attribute does not exist in the
     * source file; it's mandatory!
     */
    private String getNumberOfUsers(  ) throws XDocletException {
        return getTagValue(XDocletTagSupport.FOR_METHOD,
                           LOAD_TEST,
                           NUMBER_OF_USERS,
                           null,
                           null,
                           false,
                           true);
    }

    /**
     * Helper method that retrieves the number of iterations each user
     * of a <code>LoadTest</code> must execute.
     *
     * @return the number of iterations to use for a load test. If the
     * value is not specified in the source file a default value of
     * 1 is used.
     */
    private String getNumberOfIterations(  ) throws XDocletException {
        return getTagValue(XDocletTagSupport.FOR_METHOD,
                           LOAD_TEST,
                           NUMBER_OF_ITERATIONS,
                           null,
                           "1",
                           false,
                           false);
    }

    /**
     * Helper method that retrieves the max allowed time for a <code>
     * TimedTest</code> to execute. This is another example of a mandatory
     * attribute.
     *
     * @return the max allowed time for a test to execute.
     * @throws XDocletException
     */
    private String getMaxElapsedTime(  ) throws XDocletException {
        return getTagValue(XDocletTagSupport.FOR_METHOD,
                           TIMED_TEST,
                           MAX_ELAPSED_TIME,
                           null,
                           null,
                           false,
                           true);
    }

    /**
     * Helper method that retrieves whether or not the <code>TimedTest</code>
     * should wait for the JUnit test method to complete before throwing
     * an exception if the time has elapsed. This method shows an example
     * of setting up two valid default values.
     *
     * @return 'true' if the timed test should wait for completion of the
     * JUnit test method before throwing an exception; 'false' if an
     * exception should be raised immediately.
     */
    private String getWaitForCompletion(  ) throws XDocletException {
        return getTagValue(XDocletTagSupport.FOR_METHOD,
                           TIMED_TEST,
                           WAIT_FOR_COMPLETION,
                           "true,false",
                           "false",
                           false,
                           false);
    }
}

9.11.4 See Also

Recipe 9.10 shows how to create a custom Ant Doclet subtask to generate JUnitPerf tests. Recipe 9.12 shows how to create a custom template file that uses the JUnitPerfDoclet tag handler. Recipe 9.13 shows how to create an XDoclet xdoclet.xml file used to define information about your code generator. Recipe 9.14 shows how to package JUnitPerfDoclet into a JAR module. Chapter 8 provides information on the JUnitPerf tool and how to update your Ant buildfile to invoke JUnitPerfDoclet.