ONJava.com -- The Independent Source for Enterprise Java
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Developing with JAXB and Ant, Part 2
Pages: 1, 2

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.