Friday, May 6, 2011

Documenting Groovy with Groovydoc

Groovydoc was introduced in 2007 to provide for Groovy what Javadoc provides for Java. Groovydoc is used to generate the API documentation for the Groovy and Java classes that compose the Groovy language. In this post, I look at invoking Groovydoc via the command-line and via Groovy-provided custom Ant task.


Groovy and Java Source Code with Groovydoc/Javadoc Comments

I will use adapted versions of the Groovy script and classes first introduced in my blog post Easy Groovy Logger Injection and Log Guarding to demonstrate Groovydoc. The main Groovy script and the Groovy classes from that post have been modified to include more Javadoc-style comments to better demonstrate Groovydoc in action. The revised script and associated classes are shown in the next code listings.

demoGroovyLogTransformation.groovy
#!/usr/bin/env groovy
/**
 * demoGroovyLogTransformation.groovy
 *
 * Grab SLF4J, Log4j, and Apache Commons Logging dependencies using @Grab and
 * demonstrate Groovy 1.8's injected logging handles.
 *
 * http://marxsoftware.blogspot.com/2011/05/easy-groovy-logger-injection-and-log.html
 */

// No need to "grab" java.util.logging: it's part of the JDK!

/*
 * Specifying 'slf4j-simple' rather than 'slf4j-api' to avoid the error
 * "Failed to load class "org.slf4j.impl.StaticLoggerBinder" that is caused by
 * specifying no or more than one of the actual logging binding libraries to
 * be used (see http://www.slf4j.org/codes.html#StaticLoggerBinder). One should
 * be selected from 'slf4j-nop', 'slf4j-simple', 'slf4j-log4j12.jar',
 * 'slf4j-jdk14', or 'logback-classic'. An example of specifying the SLF4J
 * dependency via @Grab is available at
 * http://mvnrepository.com/artifact/org.slf4j/slf4j-api/1.6.1.
 */
@Grab(group='org.slf4j', module='slf4j-simple', version='1.6.1')

/*
 * An example of specifying the Log4j dependency via @Grab is at
 * http://mvnrepository.com/artifact/log4j/log4j/1.2.16.
 */
@Grab(group='log4j', module='log4j', version='1.2.16')

/*
 * An example of specifying the Apache Commons Logging dependency via @Grab is at
 * http://mvnrepository.com/artifact/commons-logging/commons-logging-api/1.1.
 */
@Grab(group='commons-logging', module='commons-logging-api', version='1.1')


/*
 * Run the tests...
 */
int headerSize = 79
printHeader("java.util.logger", headerSize)
def javaUtilLogger = new JavaUtilLoggerClass()
printHeader("Log4j", headerSize)
def log4jLogger = new Log4jLoggerClass()
printHeader("SLF4j", headerSize)
def slf4jLogger = new Slf4jLoggerClass()
printHeader("Apache Commons", headerSize)
def commonsLogger = new ApacheCommonsLoggerClass()


/**
 * Print header with provided text.
 *
 * @param textForHeader Text to be included in the header.
 * @param sizeOfHeader Number of characters in each row of header.
 */
def printHeader(final String textForHeader, final int sizeOfHeader)
{
   println "=".multiply(sizeOfHeader)
   println "= ${textForHeader}${' '.multiply(sizeOfHeader-textForHeader.size()-4)} ="
   println "=".multiply(sizeOfHeader)
}

JavaUtilLoggerClass.groovy
import groovy.util.logging.Log

/**
 * Sample Groovy class using {@code @Log} to inject java.util.logging logger
 * into the class.
 */
@Log
class JavaUtilLoggerClass
{
   /**
    * Constructor.
    */
   public JavaUtilLoggerClass()
   {
      println "\njava.util.logging (${log.name}: ${log.class}):"
      log.info "${this.printAndReturnValue(1)}"
      log.finer "${this.printAndReturnValue(2)}"
   }

   /**
    * Print provided value and then return it as part of String indicating part
    * of JDK's java.util.logging.
    *
    * @param newValue Value to be printed and included in return String.
    * @return String indicating newValue and JDK for java.util.logging.
    */
   public String printAndReturnValue(int newValue)
   {
      println "JDK: Print method invoked for ${newValue}"
      return "JDK: ${newValue}"
   }
}

Log4jLoggerClass.groovy
import groovy.util.logging.Log4j

import org.apache.log4j.Level

/**
 * Sample Groovy class using {@code @Log4j} to inject Log4j logger
 * into the class.
 */
@Log4j
class Log4jLoggerClass
{
   /**
    * Constructor.
    */
   Log4jLoggerClass()
   {
      // It is necessary to set logging level here because default is FATAL and
      // we are not using a Log4j external configuration file in this example
      log.setLevel(Level.INFO)
      println "\nLog4j Logging (${log.name}: ${log.class}):"
      log.info "${this.printAndReturnValue(1)}"
      log.debug "${this.printAndReturnValue(2)}"
   }

   /**
    * Print provided value and then return it as part of String indicating part
    * of Log4j.
    *
    * @param newValue Value to be printed and included in return String.
    * @return String indicating newValue and Log4j.
    */
   public String printAndReturnValue(int newValue)
   {
      println "Log4j: Print method invoked for ${newValue}"
      return "Log4j: ${newValue}"
   }
}

Slf4jLoggerClass.groovy
import groovy.util.logging.Slf4j

/**
 * Sample Groovy class using {@code @Slf4j} to inject Simple Logging Facade for
 * Java (SLF4J) logger into the class.
 */
@Slf4j
class Slf4jLoggerClass
{
   /**
    * Constructor.
    */
   public Slf4jLoggerClass()
   {
      println "\nSLF4J Logging (${log.name}: ${log.class}):"
      log.info "${this.printAndReturnValue(1)}"
      log.debug "${this.printAndReturnValue(2)}"
   }

   /**
    * Print provided value and then return it as part of String indicating part
    * of SLF4J logging.
    *
    * @param newValue Value to be printed and included in return String.
    * @return String indicating newValue and SLF4J.
    */
   public String printAndReturnValue(int newValue)
   {
      println "SLF4J: Print method invoked for ${newValue}"
      return "SLF4J: ${newValue}"
   }
}

ApacheCommonsLoggerClass.groovy
import groovy.util.logging.Commons

/**
 * Sample Groovy class using {@code @Commons} to inject Apache Commons logger
 * into the class.
 */
@Commons
class ApacheCommonsLoggerClass
{
   /**
    * Constructor. 
    */
   public ApacheCommonsLoggerClass()
   {
      println "\nApache Commons Logging (${log.name}: ${log.class}):"
      log.info "${this.printAndReturnValue(1)}"
      log.debug "${this.printAndReturnValue(2)}"
   }

   /**
    * Print provided value and then return it as part of String indicating part
    * of Apache Commons Logging.
    *
    * @param newValue Value to be printed and included in return String.
    * @return String indicating newValue and Apache Commons Logging.
    */
   public String printAndReturnValue(int newValue)
   {
      println "Commons: Print method invoked for ${newValue}"
      return "Commons: ${newValue}"
   }
}

In addition to the above Groovy script and classes, I also use a new Java class here to illustrate that Groovydoc works on Java classes as well as Groovy classes. The Java class doesn't do much besides providing the Javadoc comments to be processed by Groovydoc.

DoNothingClass.java
/**
 * Class that does not do anything, but is here to be a Java class run through
 * groovydoc.
 */
public class DoNothingClass
{
   /**
    * Simple method that returns literal "Hello _addressee_!" string where
    * _addressee_ is the name provided to this method.
    *
    * @param addressee Name for returned salutation to be addressed to.
    * @return "Hello!"
    */
   public String sayHello(final String addressee)
   {
      return "Hello, " + addressee;
   }

   /**
    * Main executable function.
    */
   public static void main(final String[] arguments)
   {
      final DoNothingClass me = new DoNothingClass();
      me.sayHello("Dustin");
   }

   /**
    * Provide String representation of this object.
    *
    * @return String representation of me.
    */
   @Override
   public String toString()
   {
      return "Hello!";
   }
}


Running Groovydoc on the Command Line

With the Groovy script, Groovy classes, and Java class shown above ready to go, it is time to turn attention to running Groovydoc against these classes and script. As is the case with Javadoc, Groovydoc can be run from the command line. The command for running Groovydoc against the above classes and scripts (assuming they are all in the same directory in which the command is run) looks something like this:

groovydoc -classpath C:\groovy-1.8.0\lib\ -d output -windowtitle "Groovy 1.8 Logging Example" -header "Groovy 1.8 Logging (Inspired by Actual Events)" -footer "Inspired by Actual Events: Logging in Groovy 1.8" -doctitle "Logging in Groovy 1.8 Demonstrated"  *.groovy *.java

The above command is all run on one line. However, for improved readability, I have added line breaks to break the command down.

groovydoc -classpath C:\groovy-1.8.0\lib\
          -d output
          -windowtitle "Groovy 1.8 Logging Example"
          -header "Groovy 1.8 Logging (Inspired by Actual Events)"
          -footer "Inspired by Actual Events: Logging in Groovy 1.8"
          -doctitle "Logging in Groovy 1.8 Demonstrated"
          *.groovy *.java

The groovydoc command's parameters look familiar to anyone who has used javadoc from the command line. The last part of the command specifies that groovydoc should be run against Groovy and Java code.


Running Groovydoc from Ant

Groovydoc can also be easily accessed via a custom Ant task as described in the Groovy User Guide. It is fairly easy to apply the groovydoc Ant task by first setting up the appropriate taskdef and then by using that defined tag. This is demonstrated in the following XML snippet from a relevant build.xml file.

Portions of an Ant build.xml File Demonstrating groovydoc Task
 <taskdef name="groovydoc"
            classname="org.codehaus.groovy.ant.Groovydoc"
     classpathref="groovydocpath" />

   <target name="generategroovydoc" description="Generate Groovydoc-based documentation">
      <mkdir dir="${groovydoc.dir}" />
      <groovydoc destdir="${groovydoc.dir}"
                 sourcepath="."
                 packagenames="**.*"
                 use="true"
                 windowtitle="Groovy 1.8 Logging Example"
                 doctitle="Logging in Groovy 1.8 Demonstrated"
                 header="Groovy 1.8 Logging (Inspired by Actual Events)"
                 footer="Inspired by Actual Events: Logging in Groovy 1.8"
   overview="override.html"
                 private="false">
         <link packages="java.,org.xml.,javax.,org.xml." href="http://download.oracle.com/javase/6/docs/api"/>
         <link packages="groovy.,org.codehaus.groovy."   href="http://groovy.codehaus.org/api/"/>
      </groovydoc>
   </target>

The portion of the Ant build.xml shown above is roughly equivalent to that used on the command line. Having Groovydoc available via Ant is important because it makes it easier to integrate building of Groovy documentation from Ant-based build systems.


Groovydoc Generated Documentation

Because each approach to generating Groovy documentation via Groovydoc (command line or Ant-based) works about the same as the other, I'll now focus on the HTML output that could come from either approach. The next series of screen snapshots shows the generated documentation starting with the main page, followed by the DefaultPackage page (I lazily left the script, Groovy classes, and Java class in the current directory and without any package declaration), followed respectively by the output for the Groovy script, for an example Groovy class, and for the contrived Java class. The last three images help to differentiate between the output for a Groovy Script versus a Groovy class versus a Java class.

Groovydoc Main Page Example

Groovydoc Output for Example Package (DefaultPackage)

Groovydoc Output for Example Groovy Script

Groovydoc Output for Example Groovy Class

Groovydoc Output for Example Java Class

Several observations can be made from the Groovydoc output shown above. First, the generated documentation for the Groovy script only documented the methods defined in the script (including the implicit main method). What is not so obvious from the static images above is that, in fact, no Groovydoc output is created for a script at all unless at least one method is explicitly defined in the script. If one method is defined in the script, then Groovydoc output is generated for any defined methods and for the implicit main method. The option -nomainforscripts can be passed to Groovydoc to have no Groovydoc generated for the implicit main method. The output of adding this option is shown next (note that the main's documentation is no longer displayed).


The -nommainforscripts option is nice because we often don't want the main function to be implicitly documented for our scripts. Indeed, the main function is typically "hidden" from us as script writers and maintainers.

A second observation from looking at Groovydoc-generated output is that the generated output distinguishes between Groovy and Java source code. Groovy scripts and classes are labeled with "[Groovy]" and Java classes are labeled with "[Java]." This is also evident in the Groovydoc-generated Groovy API documentation where this features makes it easy to identify that groovy.sql.Sql and AntBuilder are Java classes while JmxBuilder and SwingBuilder are Groovy classes.

A third observation is the numerous methods that Groovy script's automatically inherit from groovy.lang.Script and from groovy.lang.GroovyObjectSupport. These are displayed in the next screen snapshot taken directly from Groovydoc's output for the script shown above.



Conclusion

I don't find myself commenting Groovy scripts with Javadoc/Groovydoc style comments as often as I might comment a Groovy or Java class, but it is nice to be able to do so in some cases. I frequently use the various API documentation sets for Groovy and appreciate their availability. It is nice to be able to easily generate documentation for Groovy in a very similar manner to that used for Java.

No comments: