Ant is a powerful tool for automating tasks in Java development, and can be used quite effectively when dealing with development tools that have odd requirements, or special features like code generation. This article will show the use of Ant when working with a tool like the Java API for XML Binding (JAXB), and in the process show how JAXB can be used with packages, a feature not demonstrated in the examples provided in the JAXB distribution.
Working with XML in Java can be greatly simplified when the developer can utilize type-safe classes that directly reflect the XML tree, rather than abstract DOM (or JDOM) trees that require the elements and attributes to be analyzed. JAXB is being designed and worked on by the Java Community Process, to provide an API and toolset for generating Java source files from an XML DTD and some additional schema specification. JAXB's optimal use is in situations where developers are working with already-existing XML data object specifications. Even in its current early access state, JAXB is quite powerful, with a lot of potential for projects that use XML.
Although the examples provided with JAXB are relatively comprehensive, they don't necessarilly reflect the ideal environments and approaches that Java application developers should use. JAXB provides the means to work with packages, for example, but the distribution provides no examples of such use. Java developers are (or should be) used to using a build tool to automate tasks, and such tools will be useful in automating the generation of Java source files from DTD and XJS files.
Ant is the build tool developed and used by the Jakarta project, a group under the Apache banner, who have been developing a number of open source Java tools and libraries for client-server development (including Tomcat, the Servlet/JSP reference implementation). Though the example here uses JAXB, the technique is also workable for other code-generation tools, such as IDL or RMI. We'll work with refactoring the checkbook example from the JAXB distribution to use packages.
An important step is to lay out a development hierarchy that will separate the generated source files from the source files written by the developer. The user and the build system should be able to clean up and delete auto-generated files without worrying about accidentally deleting the files they themselves have written.
The base hierarchy contains just the src directory and the data
definitions directory (called datadefs in this example). The src
directory will get the three .java files that aren't generated
(CheckbookApp, CheckbookBalance, and TransDate), and datadefs will
get the checkbook.dtd and checkbook.xjs files. The test data remains
in the root directory (strictly as a simplification here). The build
system will create the other directories as needed in its init target:
<project name="checkbook" default="test">
<target name="init">
<tstamp/>
<mkdir dir="docs"/>
<mkdir dir="gensrc"/>
<mkdir dir="build"/>
</target>
</project>
While it's in mind, a matching clean target should be added:
<target name="clean">
<delete includeEmptyDirs="true">
<fileset dir="build"/>
<fileset dir="gensrc"/>
<fileset dir="docs"/>
</delete>
</target>
The directory hierarchy:
root
|- src
| |- checkbook
| |- convertors
| | |- TransDate.java
| |- demos
| |- CheckbookApp.java
| |- CheckbookBalance.java
|- datadefs
| |- checkbook.xjs
| |- checkbook.dtd
|- checkbook.xml
|- march.xml
|- build.xml
This could be modified to also delete any generated data files, such as the
_new XML files made by the test program, or text editor backup files.
|
Next, we add the target to use JAXB to generate the .java files from the DTD and XJS files. The classname was determined by examing the XJC shell script that comes JAXB. This Ant buildfile will actually be more portable than the example used with the distribution, as that file doesn't include a .bat script for Windows systems.
<target name="jaxb" depends="init">
<java fork="yes"
classname="com.sun.tools.xjc.Main"
dir="." classpathref="jaxb.class.path">
<arg value="datadefs/checkbook.dtd"/>
<arg value="datadefs/checkbook.xjs"/>
<arg value="-d"/>
<arg value="gensrc"/>
</java>
</target>
In order for this to work, some properties need to be set up:
<!-- define the home for the JAXB distribution -->
<property name="JAXBHOME"
value="C:\javalibs\jaxb-1.0-ea"/>
<path id="jaxb.class.path">
<fileset dir="${JAXBHOME}/lib">
<include name="**.jar"/></fileset>
</path>
<path id="build.class.path">
<fileset dir="${JAXBHOME}/lib">
<include name="jaxb-rt-1.0-ea.jar"/></fileset>
</path>
<path id="run.class.path">
<fileset dir="${JAXBHOME}/lib">
<include name="jaxb-rt-1.0-ea.jar"/></fileset>
<pathelement path="build"/>
</path>
The JAXB classpath needs to include all of the .jar files, since it calls the compiler as well as the runtime. The build.class.path only needs the runtime .jar file, but the run.class.path, which needs to find the .class
files of the application to run its tests, needs to also include the build
directory. These can, of course, be modified to add COTS and other .jar
files needed for the application. JAXB includes a pre-Crimson XML parser in
its runtime, so having an XML parser in the classpath is not required. In
future releases, it should be expected that JAXB will likely depend on JAXP
as a separate download, or JAXB will be included in future releases of the
XMLPack.
While the -d and gensrc arguments specify that the gensrc directory will be
the root directory of the generation, they do not specifically specify that
the generated files will belong to a specific package. This has to be done
by editing the checkbook.xjs file and adding a line to the definition, right below the XML-java-binding-schema tag:
<options package="checkbook"/>
The generated files will be put into the directory gensrc/checkbook, and,
fortunately, the JAXB system will automatically create the subdirectories as
needed, so that the user doesn't have to add them to the mkdir set in the init target.
|
Related Reading
Java and XML |
The three .java files in the src directory ("the developer's files") need to be modified by adding import checkbook.*; to each, so that they can find the data sources in their new package.
Next, the .java files, both the developer's and the generated ones,
need to be compiled. This requires using the src child element of the
javac task, rather than the srcdir attribute. Separating the task of
compiling into two separate tasks or targets doesn't work: the two are
interdependent -- compiling the src directory needs the generated
java sources, but compiling the generated sources needs the converters
in the src directory. It may be possible to separate the task into
three compilations (the converters, then the generated files, then
finally the application files that use the generated data), but that
seems overkill. Fortunately, Ant provides a way out: you can have
multiple src child elements to the javac task, and Ant considers
them all equal.
<target name="build" depends="jaxb">
<javac destdir="build" includes="**/*.java"
includeAntRuntime="false">
<src path="src"/>
<src path="gensrc"/>
<classpath refid="build.class.path"/>
</javac>
</target>
I choose to turn the Ant runtime off, in case I have different
implementations of some of the Ant classes (such as XML parsers) and also to
keep Ant from leaking in my default CLASSPATH, which may point to working
versions of the current application and keep the system from compiling
against the correct versions of COTS or other external
.jar files.
It would also be possible to specify the two (or more) source directories in
the srcdir attribute (src="src:gensrc") by separating them with colons, but
one of the points of XML, and by extention, Ant's build.xml files, is to make
human-readable build files that are computer-processable, and let Ant deal
with the complex syntax involved in sending multiple parameters to the javac
command line instead of the user. So my general recommendation is to use
child attributes or separate path specifications as much as possible over
putting every specification in the task's attribute lists. The reader may
also note the specification of .java files in the includes attribute;
if you leave this off, Ant may attempt to compile all files in the src
directories, causing unnecessary failures.
If you attempt to compile now, you'll run into a failure that unfortunately
can't be worked around in an ideal way. The TransDate class "can't
be found" by the generated .java files. This seems odd, since you know
TransDate.java is right there in src, so you might be concerned that the
multiple-src feature of Ant isn't working (or something to that effect). In
fact, it's a side effect of Java's package feature. When a class is declared
to be in a package, it cannot reference a class in the "root" package
(i.e., a class not in a package) without a specific import statement to that
class (and import *; doesn't work). JAXB gives no means of adding that
kind of import to the generated file, and it's rather unefficient (and
tricky) to try to come up with some way of adding that line to each
generated .java file.
So the alternative, and in fact the better design, is to move the TransDate
class into its own package, checkbook.convertors. While we're at it, we'll go
ahead and move the other two Checkbook files to the package checkbook.demos
(remembering that we need to both move the file and add the package
declaration).
Since TransDate is now in another package, we have to go back and edit
the checkbook.xjs file again, changing
<conversion name="TransDate"
type="java.util.Date"
parse="TransDate.parseDate"
print="TransDate.printDate"/>
to
<conversion name="TransDate"
type="java.util.Date"
parse="checkbook.convertors.TransDate.parseDate"
print="checkbook.convertors.TransDate.printDate"/>
Finally, this change requires editing CheckbookApp.java, which references TransDate, to add the import checkbook.convertors.*; line.
Now, all the files should compile, and we can add a target to run the demo, with a simple Java task.
<target name="test" depends="build">
<java fork="yes"
classname="checkbook.demos.CheckbookApp"
dir="." classpathref="run.class.path">
</java>
</target>
Note how this is using the run.class.path, which makes sure that the built .class files are included in the .classpath. I almost always
use fork in my Java tasks, in case of accidental uses of System.exit().
At this point, the task is done, with the possible exception of adding
targets to make distribution .jar files or the Javadocs. The former
is trivial, but making Javadocs when the sources are coming from more
than one directory is less so, since this uses a different (though more
standard) syntax than the src elements of javac. (I expect that in Ant 2.0, javac's syntax will be modified to match and bring all tasks into conformity.) The Javadoc task uses Ant's path syntax, the same that was used in specifying the classpath references above.
<path id="src.path.list">
<pathelement path="src"/>
<pathelement path="gensrc"/>
</path>
With that in place, we can reference it with the Javadoc task:
<target name="javadoc" depends="build">
<javadoc sourcepathref="src.path.list"
packagenames="checkbook,checkbook.convertors,checkbook.demos"
windowtitle="Checkbook Example API Documentation"
destdir="docs">
</javadoc>
</target>
It would be useful, in a larger environment, to specify the package names by some other means than listing them there (or as a property), or in an external text file, but Ant doesn't currently provide that feature. When running this target, a large number of warnings will show up, because Javadoc can't find the JAXB runtime sources or Javadocs. These can be ignored, or you can add a link attribute to your own Web page that has a copy of the files. Javasoft.com does not publish the Javadocs themselves so, unlike with the JDK, you can't link to a copy there. This may change when the package moves out of early access.
In the next article of this series, we will look at ways of embedding JAXB
into the build process more deeply by installing it into the Ant lib and
writing new Ant tasks and mappers to keep Ant from regenerating the .java
files on every run. Ant provides default mappers to specify one-to-one and
many-to-one relationships between source and target files, but does not
address the issue of one-to-many relationships that JAXB uses. That article may also
address the idea of making a task that creates the packagelist file
automatically by analyzing the directories under the source tree.
Joseph Shelby is Software Engineer, ISX Corporation, Arlington, VA.
Return to ONJava.com.
Copyright © 2007 O'Reilly Media, Inc.