5.1 | Working with files | |
5.2 | Introducing mappers | |
5.3 | Modifying files as you go | |
5.4 | Preparing to package | |
5.5 | Creating JAR files | |
5.6 | Testing with JAR files | |
5.7 | Creating Zip files | |
5.8 | Packaging for Unix | |
5.9 | Working with resources | |
5.10 | Summary |
We can now compile and test our diary classes, using Ant, <javac>, and <junit>. This code can be turned into a JAR library. It can be used inside our application, or it can be redistributed for other people to use.
This brings us and our build file to the next problem: packaging a program for reuse and redistribution. We want to take the compiled classes and create a JAR file that can itself be bundled into some source and binary redistribution packages—such as Zip and tar files—for different platforms. We will then be able to execute the JAR file and upload the Zip and tar files to servers. We are effectively releasing our diary as a library, on the basis that having passed its tests, it’s ready for use.
What else does a project need to do before releasing a Java program?
Ant can handle the activities in figure 5.1. It can take the source files and create .class files and JavaDoc documentation, then package everything up as redistributables. Along the way it can copy, move, and delete files and directories.
The build file can already compile the source and run the tests; in this chapter we’ll look at the rest of the packaging problem. We’ll start with file system operations to get everything into the right place.
The basis of the packaging and deployment process is copying and moving files around. Ant has a set of tasks to do this, most of which operate on filesets. We can use them to prepare directories and files for the packaging steps in the build.
Before we can distribute, we need a destination for our distributable files. Let’s create a subdirectory dist with another doc for documentation under it. As usual, we declare these locations through properties to provide override points.
<property name="dist.dir" location="dist" /> <property name="dist.doc.dir" location="${dist.dir}/doc" /> <mkdir dir="${dist.dir}"/> <mkdir dir="${dist.doc.dir}"/>
This XML will create all the distribution directories. The <mkdir> task creates all parent directories in its dir attribute, so when the task is executed with dir="dist/doc", the whole directory tree would be created on demand.
This same recursive creation applies to deletion, where the entire distribution directory can be deleted all at once.
We’ve been deleting files since chapter 2, using the <delete> task. This task can delete an individual file with a single file attribute:
<delete file="${dist.doc.dir}/readme.txt" />
It can just as easily delete an entire directory with
<delete dir="${dist.dir}" />
This task is dangerous, as it can silently delete everything in the specified directory and those below it. If someone accidentally sets the dist.dir property to the current directory, then the entire project will be destroyed. Be careful of what you delete.
For more selective operations, <delete> takes a fileset as a nested element, so you can specify a pattern, such as all backup files in the source directories:
<delete> <fileset dir="${src.dir}" includes="*~" defaultexcludes="false" /> </delete>
This fileset has the attribute defaultexcludes="false". Usually, filesets ignore the editor- and SCM-generated backup files that often get created, but when trying to delete such files you need to turn off this filtering. Setting the defaultexcludes attribute to false has this effect.
Three attributes on <delete> handle failures: quiet, failonerror, and deleteonexit. The task cannot delete files if another program has a lock on the file, so deletion failures are not unheard of, especially on Windows. When the failonerror flag is true, as it is by default, Ant halts the build with an error. If the flag is false, then Ant reports the error before it continues to delete the remaining files. You can see that something went wrong, but the build continues:
<delete defaultexcludes="false" failonerror="false" > <fileset dir="${dist.dir}" includes="**/"/> </delete>
The quiet option is nearly the exact opposite of failonerror. When quiet="true", errors aren’t reported and the build continues. Setting this flag implies you don’t care whether the deletion worked or not. It’s the equivalent of rm -q in Unix. The final flag, deleteonexit, tells Ant to tell the JVM to try to delete the file again when the JVM is shut down. You can’t rely on this cleanup being called, but you could maybe do some tricks here, such as marking a file that you know is in use for delayed deletion. Things may not work as expected on different platforms or when Ant is run from an IDE.
There’s also a verbose flag to tell the task to list all the files as it goes. This can be useful for seeing what’s happening:
<delete failonerror="false" verbose="true"> <fileset dir="${dist.dir}" includes="**/"/> </delete>
Deleting files is usually a housekeeping operation. Its role in packaging is to clean up destination directories where files can go before adding the directory contents to JAR, Zip, or tar archives. Create a clean directory with a <delete> command and a <mkdir> command, then copy all the files to be packaged into this directory tree.
The task to copy files is, not surprisingly, <copy>. At its simplest, you can copy files from one place to another. You can specify the destination directory; the task creates it and any parent directories if needed:
<copy file="readme.html" todir="${dist.doc.dir}"/>
You can also give it the complete destination filename, which renames the file during the copy:
<copy file="readme.html" tofile="${dist.doc.dir}/README.HTML"/>
To do a bulk copy, declare a fileset inside the copy task; all files will end up in the destination directory named with the todir attribute:
<copy todir="${dist.doc.dir}"> <fileset dir="doc" > <include name="**/*.*"/> </fileset> </copy>
By default, <copy> is timestamp-aware; it copies only the files that are newer than those of the destination. At build time this is what you want, but if you’re using the task to install something over a newer version, set overwrite="true". This will always overwrite the destination file.
Copied files’ timestamps are set to the current time. To keep the date of the original file, set preservelastmodified="true". Doing so can stop other tasks from thinking that files have changed. Normally, it isn’t needed.
If you want to change the names of files when copying or moving them, or change the directory layout as you do so, you can specify a <mapper> as a nested element of the task. We’ll cover mappers in section 5.2.
One limitation of Ant is that <copy> doesn’t preserve Unix file permissions, because Java doesn’t let it. The <chmod> task can be used to set permissions after a copy—a task that is a no-op on Windows—so it can be inserted where it’s needed. Similarly, Ant cannot read permissions when creating a tar archive file, a problem we’ll solve in a different way.
Related to the <copy> task is the <move> task, which enables you to move or rename files.
Ant’s <move> task can move files around. It first tries to rename the file or directory; if this fails, then it copies the file and deletes the originals. An unwanted side effect is that if <move> has to copy, Unix file permissions will get lost.
The syntax of this task is nearly identical to <copy>, as it’s a direct subclass of the <copy> task, so any of the examples listed in section 5.1.1 can be patched to move files instead:
<move file="readme.txt" todir="${dist.doc.dir}"/>
As with <copy>, this task uses timestamps to avoid overwriting newer files unless overwrite="true".
The <move> task is surprisingly rare in build files, as copying and deleting files are much more common activities. Its main role is renaming generated or copied files, but since <copy> can rename files during the copy process and even choose a different destination directory, there’s little need for the task.
We’ve shown how filesets can select files to copy or move, but what if you want to rename them as they’re moved? What if you want to flatten a directory so that all JAR files are copied into one single directory? These are common operations in preparing files for packaging. To do these operations you need mappers.
Ant’s mappers generate a new set of filenames from source files. Any time you need to move sets of files into a new directory hierarchy, or change parts of the filename itself, such as an extension, look for an appropriate mapper. Table 5.1 shows the built-in mapper types. They are used by <uptodate>, <move>, <copy>, <apply>, and several other tasks.
Type |
Description |
---|---|
identity | The target is identical to the source filename. |
flatten | Source and target filenames are identical, with the target filename having all leading directory paths stripped. |
merge | All source files are mapped to a single target file specified in the to attribute. |
glob | A single asterisk (*) used in the from pattern is substituted into the to pattern. Only files matching the from pattern are considered. |
package | A subclass of the glob mapper, package functions similarly except that it replaces path separators with the dot character (.) so that a file with the hierarchical package directory structure can be mapped to a flattened directory structure while retaining the package structure in the filename. |
regexp | Both the from and to patterns define regular expressions. Only files matching the from expression are considered. |
unpackage | Replaces dots in a Java package with directory separators. |
composite | Applies all nested mappers in parallel. |
chained | Applies all nested mappers in sequence. |
filter | Applies a list of ‘pipe’ commands to each filename. |
scriptmapper | Creates an output filename by running code in a scripting language. |
Mappers are powerful, and it’s worthwhile looking at them in detail. If a project has any need to rename files and directories or move files into a different directory tree, a mapper will probably be able to do it. Let’s explore them in some more detail.
The first mapper is the identity mapper, which is the default mapper of <copy> and <move>. It’s used when a task needs a mapper, but you don’t need to do any filename transformations:
<identitymapper/>
Because it’s the default mapper of <copy>, the following declarations are equivalent:
<copy todir="new_web"> <fileset dir="web" includes="**/*.jsp"/> <identitymapper/> </copy> <copy todir="new_web"> <fileset dir="web" includes="**/*.jsp"/> </copy>
It’s fairly rare to see the identity mapper because you get it for free.
The next mapper, the flatten mapper, is used when collecting files together in a single directory, such as when collecting JAR files to go into the WEB-INF/lib directory of a web application.
The flatten mapper strips all directory information from the source filename to map to the target filename. This is one of the most useful mapping operations, because it collects files from different places and places them into a single directory. If we wanted to copy and flatten all JAR files from a library directory hierarchy into a single directory ready for packaging, we would do this:
<copy todir="dist/lib"> <fileset dir="lib" includes="**/*.jar"/> <flattenmapper /> </copy>
If multiple files have the same name in the source fileset, only one of them will be mapped to the destination directory—and you cannot predict which one.
Although it copies everything to a single directory, the flatten mapper doesn’t rename files. To do that, use either the glob or regexp mapper.
The very useful glob mapper can do simple file renaming, such as changing a file extension. It has two attributes, to and from, each of which takes a string with a single asterisk (*) somewhere inside. The text matched by the pattern in the from attribute is substituted into the to pattern:
<globmapper from="*.jsp" to="*.jsp.bak"/>
The glob mapper is useful for making backup copies of files by copying them to new names, as shown in the following example. Files not matching the from pattern are ignored.
<copy todir="new_web"> <fileset dir="web" includes="**/*.jsp"/> <globmapper from="*.jsp" to="*.jsp.bak" /> </copy>
This task declaration will copy all JSP pages from the web directory to the new_web directory with each source .jsp file given the .jsp.bak extension.
If you have more complex file-renaming problems, it’s time to reach for the big brother of the glob mapper, the regexp mapper, which can handle arbitrary regular expressions.
The regexp mapper takes a regular expression in its from attribute. Source files matching this pattern get mapped to the target file. The target filename is built using the to pattern, with pattern substitutions from the from pattern, including