I agree, Ant 1.6 features finally make it possible to create well-structured complex build files. To the features Stefan mentions, I would add target name overriding as an important new feature because it allows you to define a standard set of targets while changing the behavior as needed for each build project. For example, if you have a distribution target that compiles and packages your code like this:
<target name="dist" depends="compile, jar"/>
<br/>
And you want to have unit tests run before you package up the distribution, you can override the 'dist' target like thus:
<target name="dist" depends="compile, junit, jar"/>
<br/>
It isn't quite OO for Ant, but it comes close.
Maintaining a standard set of target names is important for usability as Ant scripts become more complex.
To get the most flexibility out of this technique I follow two rules of thumb: The first is to leave the bodies empty on the standard targets. This enables you to add or remove behavior more easily in the overriding target. The second trick is to keep the dependencies to a minimum in the supporting (non-standard) targets and instead define them in the standard targets. For example, this:
<target name="compile" depends="javac" description="standard target"/>
<target name="javac" depends="copy.resources" description="supporting target"/>
</target>
<target name=" copy.resources" description="supporting target">
</target>
<br/>
is less flexible than this:
<target name="compile" depends="javac, copy.resources" description="standard target"/>
<target name="javac" description="supporting target"/>
</target>
<target name=" copy.resources" description="supporting target">
</target>
<br/>
because you can disable the copy.resources target without overriding the supporting javac target.
For an extensive example of using Ant 1.6 features for building and testing complex J2EE applications, see the open-source
JAM - JavaGen Ant Modules framework.