Extending BeanDoc

In the first instance, make sure that extending the tool is really what you need to do. The standard setup is very flexible and you can change the look and feel of the HTML, generate a completely different output language and manipulate graphs to a high degree without writing code to extend BeanDoc. Check the previous chapters if this sounds more like what you want to do.

Implementable interfaces

Generally it should be possible to get BeanDoc to do whatever you need by implementing either org.springframework.beandoc.output.Decorator or org.springframework.beandoc.output.Transformer or possibly both. Built in implemenations of these two interfaces do most of the work of generating documentation from the input files. The rest is handled by the ContextProcessor and DocumentCompiler implementation(s).

This chapter will explore by way of a simple example the steps involved in adding the required files to produce additional output from that already produced by the tool. If you're genuinely of need of this stuff, the API documentation is going to be your most important resource.

Our trivial example will create a Transformer that produces a CSV file (Comma Separated Values) which could be loaded into a spreadsheet or otherwise parsed by text handling tools. All of the information that we might need in such a file is probably already available in standard DOM trees after decoration by the DefaultContextProcessor, so in a slightly contrived manner we'll assume that we also need a sequential ID for each bean in order to make it necessary to implement a custom Decorator. In fact this could be done with an XSL Transformer implementation, but I'm not going to use XSL for our Transformer example.

Decorators, Transformers and DocumentCompilers use JDom API's for XML processing.

Writing Decorators

A Decorator has the ability to markup or decorate the XML elements in the DOM tree after the ContextProcessor has created the in-memory representation and performed some remedial decoration of its own. Decoration is incremental and decorators should be careful not to overwrite each other's attributes or element values. To ensure this is the case in our example, we'll use an attribute prefix of "beandocCSV" for all our attribute names.

Note: It's worth ALWAYS specifying attribute names that start with "beandoc" as some of the standard transformations ignore such attributes when actually documenting the context.

Let's start by extending the included SimpleDecorator class from the BeanDoc source. This class implements the required decorate() method for us and delegates to a decorateElement() method which gives us access to every individual element in each context file in turn.

The Decorator interface is simple with a single method to implement..

public interface Decorator {

    public void decorate(Document[] contextDocuments);
    
}

The default context processor implementation in BeanDoc will pass in the array of JDOM Document that it has built in order that the implementation might amend values or add attributes. A convenience implementation of this interface called SimpleDecorator subsequently iterates each Element of each Document and calls a protected decorateElement() method for subclasses to fill out. A subclass can specify a Filter to restrict the Element types it is presented with for decoration This is the pattern we'll follow here..

public class CsvDecorator extends SimpleDecorator {

    private static final String ATTRIBUTE_PREFIX = "beandocCSV";

    static final String ATTRIBUTE_COUNTER = ATTRIBUTE_PREFIX + "Count";

    private int count = 0;

    /**
     * specify only 'bean' elements as a Filter - we're not interested in
     * decorating anything else
     */
    public CsvDecorator() {
        setFilter(new ElementFilter("bean"));
    }
    
    /**
     * @see org.springframework.beandoc.output.SimpleDecorator#decorateElement(org.jdom.Element)
     */
    protected void decorateElement(Element element) {
        element.setAttribute(ATTRIBUTE_COUNTER, String.valueOf(++count));      
    }
}

That's it. There's nothing else for our Decorator implementation to do - we've added a sequential id to each bean in the context. All the other information we need for our transformation to the CSV file is already available.

One more important point to note when writing Decorators: you can add or change attributes and change the value of Elements, but any attempt to change the structure of the DOM by adding, removing or moving Elements will result in a runtime exception.

Writing Transformers

Our transformation could use XSLT to generate the CSV but here we'll do it on the cheap and just use a FileWriter to output our document. Satisfying the Transformer interface involves implementing a single method as shown..

public interface Transformer {

    public void transform(Document[] contextDocuments);
    
}

Here's our CSV implementation..

public class CsvTransformer implements Transformer {

    /**
     * create a new CSV file for each context file
     */
    public void transform(Document[] contextDocuments) {
        for (int i = 0; i < contextDocuments.length; i++) {
            Document doc = contextDocuments[i];
            File outputFile = new File(
                "/tmp/" + doc.getRootElement().getAttributeValue("beandocFileName") + ".csv"
            );
            
            try {
                FileWriter out = new FileWriter(outputFile);
                String csvLine = "";
                
                List beans = doc.getRootElement().getChildren("bean");
                for (Iterator iter = beans.iterator(); iter.hasNext();) {
                    Element bean = (Element) iter.next();
                    csvLine = 
                        bean.getAttributeValue(CsvDecorator.ATTRIBUTE_COUNTER) + "," +
                        bean.getAttributeValue("id") + "," + 
                        bean.getAttributeValue("class") + "\n";

                    out.write(csvLine);
                }
                out.flush();
                out.close();
                
            } catch (IOException e) {
                // should be handled properly!
                e.printStackTrace();
            }
        }
    }
}

Note: The class is instructive only, it clearly doesn't attempt to handle exceptions correctly or ensure that resources are flushed or closed under such cases.

The two classes detailed above are included in the main source tree of BeanDoc and compiled into spring-beandoc.jar. If you do want to try them out, you only need folllow the instructions in the next section.

Writing DocumentCompilers

After decoration and transformation of DOM trees, the default context processor will then offer any configured DocumentCompilers the chance to pull the output resources together. In many cases this won't be necessary as transformation will produce documents in the required format. In the case of our simple example, we don't need to implement a new DocumentCompiler.

Should you need to do so, the operation is trivial involving writing a class to implement the single interface method in similar fashion to Decorators and Transformers.

public interface DocumentCompiler {    

    public void compile(Document[] contextDocuments, File outputDir);

}

Supplying a new context definition file

If you are extending BeanDoc with your own classes such as those outlined in this chapter, you will need to instruct the ContextProcessor implementation to use them. BeanDoc uses a Spring bean factory to configure itself and the Ant task has an attribute that permits you to specify a context definition file to override the internal one. This is what you must do in order to make use of your new Decorator or Transformer implementations.

The samples directory contains a context file that includes the CSV output classes as part of the List of Decorator and Transformer implementations supplied to the processor..

<bean id="processor" class="org.springframework.beandoc.DefaultContextProcessor">
    <description>
        main processor for generating output
    </description>
    <constructor-arg index="0"><value>${input.files}</value></constructor-arg>
    <constructor-arg index="1"><value>${output.dir}</value></constructor-arg>
    <property name="decorators">
        <list>
            <ref local="csvDecorator"/>
        </list>
    </property>
    <property name="transformers">
        <list>
            <ref local="csvTransformer"/>
        </list>
    </property>   

    ...
  
</bean>

<bean id="csvDecorator" class="org.springframework.beandoc.output.CsvDecorator"/>               
    
<bean id="csvTransformer" class="org.springframework.beandoc.output.CsvTransformer"/>

...

Note: It may appear more natural to nest the "csv" beans as anonymous inner beans instead of supplying <ref> tags for them in the <list>s. Remember though that unless they are top level beans with a referenceable id, you will be unable to post-process them by setting properties in beandoc.properties.

Your <beandoc/> task in Ant's buildfile will need to be told where your alternative context definition file is, for example..

<beandoc 
    inputFiles="${basedir}/petclinic/*.xml" 
    outputDir="/some/other/outputdir/html/"
    beandocContext="${basedir}/alternative-beandoc.xml"
/>

See the chapter on running BeanDoc for more information on Ant integration.