ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Developing with JAXB and Ant, Part 2

by Joseph Shelby
03/13/2002

In the first article in this two-part series, we looked at some advanced uses of Jakarta Ant's build tool to handle automatically executing xjc, the code-from-XML generation tool from the Java API for XML Binding (JAXB). We discovered that Ant has some minor flaws in handling one-to-many file dependencies, and that the only way to guarantee that Ant calls the code generator is to make sure that Ant calls the code generator on every build. In addition, we found that Ant could also be used to solve a smaller (but more common) problem, in which you must explicity provide Javadoc with a list of packages being documented. Both of these problems can be solved, but only by writing our own Ant Task objects to handle them, as we'll see in this article.

The Javadoc Dependency Problem

Let's get used to the Ant API by first attacking the problem of having to provide an explicit list of packages to Javadoc.

Javadoc for Java1 and Java2 specifies two means to list the packages for which documentation is to be generated: as a list on the command line, or as a separate text file, one package per line. Ant adds no conveniences to these methods, unfortunately. The developer has to either keep the build.xml file up to date on the packages in the distribution, or keep an external file up to date. Both have the natural problem of falling out of sync with the actual source tree, so an automated way of setting the package list is obviously useful.

We'd like to see some usage like this (where plistgen is the package list generator):

<target name="javadoc">
<plistgen sourcepathref="src.path.list"
 packagelist="packages.txt"/>
<javadoc sourcepathref="src.path.list"
 packageList="packages.txt" 
 destdir="docs"/>
</target>

or either of the other variations the javadoc task supports -- the sourcepath attribute or sourcepath child elements.

Ant provides two very useful features to help with this. There's a Path object to represent a CLASSPATH that turns out to be useful to represent a sourcepath with multiple entries (as we saw when using the Javadoc task in the first article). Also, because Ant is open source, we can use the excellent example of applying the class in the Javadoc task implementation.

Analyzing the Javadoc task, we see a section of code that's perfect:

private Path sourcePath = null;
public Path createSourcepath() {
  if (sourcePath == null) { 
    sourcePath = new Path(project); 
  }
  return sourcePath.createPath();
}

public void setSourcepath(Path src) {
  if (sourcePath == null) {
    sourcePath = src;
  } else {
    sourcePath.append(src);
  }
}

public void setSourcepathRef(Reference r) {
  createSourcepath().setRefid(r);
}

This supports all three of the desired interfaces to the sourcepath: the reference attribute, the direct attribute, and the child elements. The set methods are for the attributes, and the create method supports the child elements, as described in the Ant user guide documentation.

Finally, we need the package listing file, and choose to support the same syntax as the Javadoc task, an attribute named packageList:

private String packageList = null;
public void setPackageList(String s) {
  packageList = s;
}

Finally, the only method we need to implement is execute(). The build.xml document is enforced in the execute method, since there isn't a specific DTD or schema to validate against, and the code can't know that a parameter hasn't been set until then.

Java and XML

Related Reading

Java and XML
Solutions to Real-World Problems
By Brett McLaughlin

We start execute() with the validation checks, throwing the BuildException to force a build failure on a syntax problem.

public void execute() {
if (packageList == null) throw new 
  BuildException("No packageList specified");
if (sourcePath == null) throw new
  BuildException("No sourcepath specified");

Next, we add some safety checks on the packageList file.

File f = new File(packageList);
if (f.exists())
try {
  boolean b = f.delete();
  if (!b) throw new
    BuildException("Unable to delete old" +
      " packageList : " + f);
} catch (BuildException b){ 
  throw b;
} catch (Exception e) { 
  // possibly a security exception
  throw new BuildException("Unable to delete " + 
    "old packageList : " + f, e);
}

We create a collection for the packages to be added, a TreeSet (auto-sorted, and no duplicates), and write out its contents using the standard java.io classes (a PrintWriter or a FileWriter).

try {
  pw = new PrintWriter(new FileWriter(f));
  Iterator i = packages.iterator();
  while (i.hasNext())
    pw.println(i.next());
  pw.flush();
  pw.close();
} catch (Exception e) {
  throw new BuildException("Unable to write " +
    packageList : " + f, e);
}

At this point, the task can be compiled, installed, and executed. When compiling this class, you want to set the include's AntRuntime to true, so that the CLASSPATH includes the Ant classes that you're deriving from and using.

Once compiled, the .class file needs to be put in a .jar, and that .jar copied into the lib directory of ANT_HOME. Now, to use our new task, one final element needs to be added to a build.xml file.

<taskdef name="plistgen"
 classname="com.isx.ant.plistgen.PListGen"/>

When the Javadoc target is run, the file should be created, but will be empty, since we haven't actually populated the packages yet.

The Path class has a number of methods, but two particularly convenient methods pretty much do everything desired. First is the toString() method, which will generate a tokenizer-separated list of the path entries, where the token is correct for the platform (either a colon or a semicolon). This can be (and is) used to set the -sourcepath parameter for the Java and Javadoc command line scripts.

Second is the static method, translatePath, which takes the token-separated list and the value of the current project to create a String array of absolute paths, using the project's basedir as the root.

For example, a call to translatePath with a project whose base is c:\dev and the string src1:src2 will return a String array with two strings, c:\dev\src1 and c:\dev\src2. The current project is a field of task that all tasks inherit.

String[] roots = Path.translatePath(project,
  sourcePath.toString());

Now we have to iterate over the roots and their descendents to find paths that represent packages. We define a package as a file that:

  1. Is a child of a root (the roots will be in the top package, and as such, can't be added to Javadoc when packages are involved).
  2. Is a directory.
  3. Has Java source files inside.

Since the usual best way to iterate down a directory tree is recursion, we'll write a recursive helper method.

for (int i = 0; i < roots.length; ++i)
  addPackageIfDirectoryWithJavaFiles("", 
    roots[i], packages);

private void addPackageIfDirectoryWithJavaFiles(
  String prefix,
  File dir, Set packages) {
  if (dir.isDirectory()) { // condition 1
    File[] f = dir.listFiles();
    boolean hasJavaFile = false;
    for (int i = 0; i < f.length; ++i) {
      String thisPrefix = prefix.equals("") ? 
                          f[i].getName() : 
                          prefix + "." + 
                          f[i].getName();
      addPackageIfDirectoryWithJavaFiles(
        thisPrefix, f[i], packages);
      // once a directory is marked as having a
      // java file, its cool, don't recheck
      if (!hasJavaFile)
        hasJavaFile = 
          f[i].getName().endsWith(".java");
    }
  // condition 2 and 3
  if (!prefix.equals("") && hasJavaFile)
    packages.add(prefix);
  }
}

The final version has the addition of logging code both at the INFO and VERBOSE levels, not included in this excerpt, but worth looking at. The log() method defaults to INFO, which is displayed as output automatically. Log calls with Project.MSG_VERBOSE will be shown when Ant is called with the -verbose option, and task writers should keep that in mind in helping their users debug their Ant buildfiles.

The JAXB Dependency Problem

Now that we're more familiar with making a task in Ant, we'll work on a new one that solves the JAXB dependency problem, where the build system shouldn't regenerate the .java files if the existing ones are complete and newer (more recently generated) than the last modification of the two source files.

What we'd like to see as the task is the following, with three parameters as attributes -- the DTD file, the xjs file, and the destination root.

<jaxb dtdFile="datadefs/checkbook.dtd"
      xjsFile="datadefs/checkbook.xjs"
      dest="gensrc"/>

This is much simpler than the Java task we used before, but at the same time gives us "type-safety" by eliminating the need to put the arguments in the right order. After we get these basics working, we can consider expanding the task to support the other parameters and options.

With these attributes, we could theoretically build an execute() method that calls the main() of the JAXB compiler (com.sun.tools.xjc.Main). I say "theoretically," because unfortunately, due to a security issue ("sealed jars") and the fact that the early access JAXB includes an XML parser that conflicts with the one in Ant (it was necessary to release an "unsealed" version of that jar to deal with this issue), we can't embed the JAXB runtime into Ant directly. So we can't break out of using the Java task we got into in the last article.

We can still add the dependency checker, but instead of building it into the task, we do things external to the JAXB task and only execute the JAXB task if a particular property is set. It would be nice to use uptodate, but that implementation (and its Mapper implementation) are all keyed to comparing a single file to a result file, and we need to compare two files simultaneously to a result file (since the two files are needed to determine what the result file is). So we have to write our own task to do something similar to uptodate.

Our new build.xml syntax will be:

<taskdef name="jaxbcheck"
 classname="com.isx.ant.jaxbtask.JaxbCheck"/>

<target name="jaxb" depends"jaxbcheck"
 if="jaxb.check.dobuild">
 <java ... as before ...>
</target>

<target name="jaxbcheck" depends="init">
 <jaxbcheck dtdFile="datadefs/checkbook.dtd"
  xjsFile="datadefs/checkbook.xjs"
  dest="gensrc"
  property="jaxb.check.dobuild"
  value="true"/>
</target>

We need to duplicate part of the JAXB algorithm by analyzing the DTD and xjs files to determine what files will be created. Then we need to compare the creation time of each of those files to the source DTD and xjs files. If a file is missing, or if any file is older than the DTD or xjs file, we set the property to regenerate the files.

So our execute() method, after checking that the attributes were all set (throwing BuildException if one is missing), adds:

File realDtdFile = 
 project.resolveFile(project.translatePath(dtdFile));
File realXjsFile = 
 project.resolveFile(project.translatePath(xjsFile));
File realDest = 
 project.resolveFile(project.translatePath(destroot));
File[] files = 
  JAXBFileDeterminer.findFiles(realDtdFile, realXjsFile,
    realDestRoot);

Now, assuming JAXBFileDeterminer does its job, the algorithm is relatively simple, based on File methods:

for (int i = 0; i < files.length; ++i)
{
  if (!files[i].exists()) { ok = false; break; }
  if ((files[i].lastModified() < 
       realDtdFile.lastModified()) ||
      (files[i].lastModified() < 
       realXjsFile.lastModified()))
    { ok = false; break; }
}
if (!ok) { // if not ok, do the build
    log("Need to rebuild", Project.MSG_VERBOSE);
    project.setProperty(property, value);
}

If the property isn't set, then the JAXB target won't execute, and the build process is much faster.

The algorithm for finding the file list from the source isn't included here, but this can be done just using the DeclHandler and ContentHandler of SAX2. However, setting up the DTD file and crimson isn't the easiest thing to do -- if there's enough interest, I may write on that topic in the future. But until then, you can look at the implementation code for this article.

Source Files for this Article

Other Resources

Joseph Shelby is Software Engineer, ISX Corporation, Arlington, VA.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.