9.10 Creating an Ant XDoclet Task

9.10.1 Problem

You want to create an Ant XDoclet task and subtask to generate a new type of file.

9.10.2 Solution

Extend xdoclet.DocletTask to create the main task, if necessary, and extend xdoclet.TemplateSubTask to create one or more subtasks.

9.10.3 Discussion

This recipe is the first part of a five-part recipe. It describes the steps needed to create a new Ant XDoclet task and subtask. We are going to create tasks for the JUnitPerfDoclet code generator.

JUnitPerfDoclet does not need a main XDoclet task. Specifically, we do not need to extend xdoclet.DocletTask because no extra functionality needs to be added. All of the functionality needed by JUnitPerfDoclet lies in the subtask. For completeness, this recipe shows how to create a Doclet task.

9.10.3.1 Creating an Ant XDoclet task

The first step is to create the main Ant XDoclet task. This task is much like the ejbdoclet task, serving as an entry point into the XDoclet engine.

Here are the steps needed to create an Ant XDoclet task:

  1. Create a new Java source file and give it a name. For this example, create a YourNewDocletTask.java file and add it to your project.

  2. Add the following import:

    import xdoclet.DocletTask;
  3. Extend the DocletTask class:

    public class YourNewDocletTask extends DocletTask {
  4. Add the necessary public getter and setter methods for attributes your task defines. For example, if your new task defines an attribute called validId, then you would have this setter method:

    public void setValidId(String id) {
        this.id = id;
    }
  5. Optionally, you may override the validateOptions( ) method if you need to validate your task.

    Typically, you override this method to ensure that the user has set the proper attributes. Here's an example:

    protected void validateOptions(  ) throws BuildException {
        super.validateOptions(  );
    
        if (this.id == null || "".equals(this.id)) {
            throw new BuildException("You must specify a valid 'id' attribute.");
        }
    }

    You should call super.validateOptions( ) to allow the base class a chance to perform validation, too. If any error occurs an org.apache.tools.ant.BuildException should be thrown.

    Another interesting feature of XDoclet is the checkClass(String class) method. This method tries to load a class specified by the given class parameter using Class.forName( ), and if the class is not on the classpath then a nice error message is printed to the console. The classpath is the one defined for the Ant task definition. This is a major improvement over earlier versions of XDoclet, where you were left sifting through Java reflection errors. Here is an example:

    protected void validateOptions(  ) throws BuildException {
        super.validateOptions(  );
    
        if (this.id == null || "".equals(this.id)) {
            throw new BuildException("You must specify a valid 'id' attribute.");
        }
    
        checkClass("com.oreilly.javaxp.xdoclet.SomeClassThatYouWant");
    }
9.10.3.2 Creating the Ant Doclet subtask

Now let's delve into creating the JUnitPerfDoclet subtask, JUnitPerfDocletSubTask. This subtask is responsible for the code generation process.

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

  2. Add the following imports:

    import xdoclet.TemplateSubTask;
    import xdoclet.XDocletException;
  3. Extend the xdoclet.TemplateSubTask class:

    public class JUnitPerfDocletSubTask extends TemplateSubTask {

    The TemplateSubTask provides the hooks necessary for XDoclet to locate a template file and start the code generation process.

  4. Set up a few constants:

    public static final String DEFAULT_TEMPLATE =
            "/com/oreilly/javaxp/xdoclet/perf/junitperf.j";
    private static final String DEFAULT_JUNIT_PERF_PATTERN = 
            "TestPerf{0}.java";

    It is a good idea to set up constants that define default values the subtask needs. The constants defined above are values specifying the JUnitPerfDoclet template file and the pattern used for the classname and filename of the generated code.

  5. Add a default constructor:

    public JUnitPerfDocletSubTask(  ) {
        setDestinationFile(DEFAULT_JUNIT_PERF_PATTERN);
        setTemplateURL(getClass(  ).getResource(DEFAULT_TEMPLATE));
    }

    A constructor should set up any default values. The only two attributes needed for our subtask are the destination file and template file. The TemplateSubTask class defines both of these attributes.

    The destination file attribute specifies the name of the generated file. If this attribute contains the substring "{0}" then XDoclet generates a new file for each source file processed. If the substring "{0}" is omitted, only one output file is generated for all source files processed. Let's look at an example.

    If the value of the destination file attribute is TestPerf{0}.java and the current class being processed is Customer, then XDoclet generates a new file named TestPerfCustomer.java. The name of the current class being processed is substituted in place of the substring "{0}". If you are familiar with the java.text package, you may have guessed that XDoclet uses the java.text.MessageFormat class to achieve the substitution. The next recipe shows how to use this technique.

    The template file attribute specifies where to locate the .xdt file. JUnitPerfDoclet, by default, loads the junitperf.xdt template file from the classpath.

  6. Override the validateOptions( ) method to validate one or more attributes:

    public void validateOptions(  ) throws XDocletException {
        super.validateOptions(  );
    
        if (getDestinationFile(  ).indexOf("{0}") == -1) {
            throw new XDocletException(
                    "The '" + getSubTaskName(  ) +
                    "' Ant Doclet Subtask attribute 'destinationFile' " +
                    "must contain the substring '{0}', which serves as a " +
                    "place holder JUnit Test name.");
            }
        }
    }

    Here the validateOptions( ) method is overridden to ensure that the "destinationFile" attribute contains the substring "{0}". An XDocletException is thrown with a friendly message if the "destinationFile" attribute does not contain the "{0}" substring. The subtask validationOptions( ) method throws an XDocletException not a BuildException. This allows the main task to handle all XDocletExceptions before halting the process.

    It is important to call super.validateOptions( ). It ensures that the base class gets a chance to perform validation it requires.

  7. The last method to implement is a convenience method for the JUnitPerf tag handler class (this class is written in the next recipe):

    public String getJUnitPerfPattern(  ) {
        return getDestinationFile(  ).
                substring(0, getDestinationFile(  ).indexOf(".java"));
    }

    This method strips off the file extension, and it is used by the JUnitPerfTagHandler.className( ) method. The next recipe examines why this is important.

Example 9-11 shows the complete example.

Example 9-11. JUnitPerfDocletSubTask
package com.oreilly.javaxp.xdoclet.perf;

import xdoclet.TemplateSubTask;
import xdoclet.XDocletException;

public class JUnitPerfDocletSubTask extends TemplateSubTask {

    public static final String DEFAULT_TEMPLATE =
            "/com/oreilly/javaxp/xdoclet/perf/junitperf.j";
    public static final String DEFAULT_JUNIT_PERF_PATTERN =
            "TestPerf{0}.java";

    public JUnitPerfDocletSubTask(  ) {
        setDestinationFile(DEFAULT_JUNIT_PERF_PATTERN);
        setTemplateURL(getClass(  ).getResource(DEFAULT_TEMPLATE));
    }

    /**
     * Used by {@link JUnitPerfTagHandler} to generate the new class name.
     * Before returning the '.java' extension is stripped off.
     *
     * @return the JUnitPerf file pattern with the '.java' extension removed.
     */
    public String getJUnitPerfPattern(  ) {
        return getDestinationFile(  ).
                substring(0, getDestinationFile(  ).indexOf(".java"));
    }

    /**
     * Overridden to validate the 'destinationFile' attribute. This attribute
     * must include a '{0}', which serves as a place holder for the JUnit
     * test name.
     */
    public void validateOptions(  ) throws XDocletException {
        super.validateOptions(  );

        if (getDestinationFile(  ).indexOf("{0}") == -1) {
            throw new XDocletException(
                    "The '" + getSubTaskName(  ) +
                    "' Ant Doclet Subtask attribute 'destinationFile' " +
                    "must contain the substring '{0}', which serves as a " +
                    "place holder JUnit test name.");
        }
    }
}

9.10.4 See Also

Recipe 9.11 shows how to create the JUnitPerfDoclet tag handler class to perform simple logic and generate snippets of code. 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.