Monday, April 25, 2005

Someone on this Javalobby post asked me to elaborate on our use of Ant and copy filtering as a build configuration mechanism, so here it is.

The problem: projects tend to have lots of different configuration files that need to be changed based on the machine or circumstance in which they are installed. Local file paths, database passwords, and host names are just a few of the settings that typically need to be set. Frequently, the same value appears in multiple configuration files allowing for the problems always associated with dual-maintenance. Also, it's often inconvenient to locate all the configuration files in the same place, so a user (a developer or configuration engineer) needs to know where they all live in order to configure them (web.xml lives in WEB-INF, log4j.xml lives in the classpath, etc.)

The solution: We use Ant to solve this problem. To address the dual-maintenance issue, we have a single file that a user needs configure called build.properties that lives in the same directory as the build.xml file. It's typically copied from another file called build.properties.template that lives in CVS. Any values in build.properties override those in build.properties.template.

Example build.properties.template:

uk.co.ourCompany.mainApp.hostname=localhost
uk.co.ourCompany.mainApp.basedir=/home/builder/src/mainApp
Example build.properties:
uk.co.ourCompany.mainApp.hostname=bender.ourCompany.co.uk
uk.co.ourCompany.mainApp.basedir=c:/src/mainApp

In our Ant build process, we have tasks that handle configuring all of our various files. They look like this:


<target name="setupWebXml">

<copy overwrite="true"
file="${uk.co.ourCompany.mainApp.basedir}/web/WEB-INF/web.xml-template"
tofile="${uk.co.ourCompany.mainApp.basedir}/web/WEB-INF/web.xml">
<filterset refid="build.propertiesFilter" />
<filterset refid="build.properties.templateFilter" />
</copy>

</target>


The filtersets are defined like this:


<!-- The following two filter sets are referenced in each of the following config targets.
First the build.properties tokens are replaced, then the build.properties.template
tokens are replaced (only if they didn't already exist in build.properties) -->
<filterset begintoken="{" endtoken="}" id="build.propertiesFilter"
description="Used to parse tokens in config files into their associated values in build.properties.">
<filtersfile file="build.properties"/>
</filterset>

<filterset begintoken="{" endtoken="}" id="build.properties.templateFilter"
description="Used to parse tokens in config files into their associated values from build.properties.template.">
<filtersfile file="build.properties.template"/>
</filterset>


Here's a snippet of the web.xml-template file that becomes web.xml:


<context-param>
<param-name>Hostname</param-name>
<param-value>{com.lollipoplearning.devEditHost}</param-value>
</context-param>


That's an artificial example for the purposes of this article; there are better ways to get the hostname, if you should be getting it at all.

Tip: At the top of each template file, put in a note (we actually have a token that also gets replaced) that warns users that the file has been generated and they should not edit it. It's very frustrating to make changes to a file and find they don't take effect because they've been clobbered by the build process.