Ant Custom Types, Conditions & Selectors
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Apache Ant 1.10.18. Every example marked as run was executed on JDK 26 (macOS) on 2026-10-16
in a scratch directory; the output is shown as it was printed, with the long absolute path of the scratch directory
shortened to <scratch>. Anything that could not be run here is labelled "not run here" in the text.
Ant has three kinds of extension. A task does something (Writing Custom Tasks
covers it, with the tidepool <checkstations> task). A listener, logger or input handler observes or answers the build
(see Listeners, Loggers & Input Handlers).
This page covers the third kind, the types: objects that a build file declares and that tasks and other types use. A
condition decides true or false, a selector decides whether a file belongs in a fileset, a resource collection produces
resources, a mapper maps one name to another, a filter reader rewrites a stream of characters, and a property delegate
resolves ${...} expressions. The Ant manual calls them custom components. The point of the design is that they are
first class: after a one-line typedef, a custom condition works in condition, fail, waitfor and any
other place a core condition works, a custom selector inside any fileset, and a custom mapper in any mapper.
Writing a type is, deliberately, the same as writing a task: a Java class with a public no-argument constructor,
setX methods that become attributes (Ant converts the string to String, int, boolean, File, Path, an enumeration
and so on), and addX, createX and addConfiguredX methods that become nested elements. What differs is the
interface the class implements, which says to Ant where the object may be used.
| You want to extend | Implement or extend | Typical use |
|---|---|---|
a condition |
|
|
a selector |
|
nested in a |
a selector container |
|
a logic of your own over nested selectors |
a resource collection |
|
|
a mapper |
|
|
a filter |
|
|
a property resolver |
|
|
The running data is the tidepool scenario described on the section landing page:
a stations CSV file (id,name,lat,lon, three stations) and a few small fixture files. The classes live in the package
io.tidepool.ant.types, are compiled with the only dependency a type needs, ant.jar from the Ant installation, and are
loaded from build/classes:
$ javac -Xlint:all -cp $ANT_HOME/lib/ant.jar -d build/classes src/io/tidepool/ant/types/*.java
The command printed no warnings. The sections below show each class and a run of a build file that uses it.
Declaring a type: typedef and componentdef
typedef makes a class known to the project under a name. It is the same as taskdef, except that it accepts any class, not
only tasks: Ant decides from the class what the element is. Two attributes are the minimum, name and classname, and a
classpath (an attribute or a nested classpath element, or classpathref) when the class is not already on Ant’s own
classpath. All the attributes of the task:
| Attribute | Meaning |
|---|---|
|
the element name; required unless |
|
the class implementing the type |
|
load many definitions at once: |
|
|
|
where to find the class |
|
the name of a class loader to create from the classpath and share between definitions (since Ant 1.5) |
|
what to do if the definition fails: |
|
an adapter class (implementing |
|
the XML namespace in which the definition lives (since 1.6), see Antlibs & Namespaces |
The build file of this page declares its types once, at the top of the project, with the classes directory as the classpath:
<property name="classes" location="build/classes"/>
<typedef name="validstation" classname="io.tidepool.ant.types.StationIdCondition" classpath="${classes}"/>
<typedef name="csvrows" classname="io.tidepool.ant.types.CsvRowsSelector" classpath="${classes}"/>
<typedef name="matchnumber" classname="io.tidepool.ant.types.MatchNumberSelectors" classpath="${classes}"/>
<typedef name="stationids" classname="io.tidepool.ant.types.StationIds" classpath="${classes}"/>
<typedef name="stationfiles" classname="io.tidepool.ant.types.StationFileMapper" classpath="${classes}"/>
<typedef name="numberlines" classname="io.tidepool.ant.types.NumberLines" classpath="${classes}"/>
A list of definitions can be kept in a properties file, one name=classname per line, and loaded with file or, from a jar or
directory on the classpath, with resource. This is the shape of an antlib without the XML. Both forms were run; with
types.properties containing the two lines station=io.tidepool.ant.types.StationIdCondition and rows=io.tidepool.ant.types.CsvRowsSelector:
<target name="typedef-file">
<typedef file="types.properties" classpath="${classes}"/>
<condition property="ok" value="yes" else="no"><station id="LIS001"/></condition>
<echo message="ok=${ok}"/>
</target>
<target name="typedef-resource">
<typedef resource="types.properties" classpath="${classes}"/>
<fileset id="wide" dir="data"><rows min="2"/></fileset>
<echo message="rows: ${toString:wide}"/>
</target>
typedef-file:
[echo] ok=yes
typedef-resource:
[echo] rows: stations.csv
(${toString:wide} is a built-in property expression that renders a referenced object as text; for a fileset it lists the
selected files.) The resource form needs types.properties inside a jar or directory on the classpath; the run copied it
into build/classes.
For packaging the definitions with their classes, so that one typedef (or one xmlns declaration) loads all of them, use an
antlib; see Antlibs & Namespaces.
Source: Apache Ant manual: Typedef
A type is not available as a task, and the error messages of typedef
A condition or selector declared with typedef is a data type: it can be declared at project level or inside a target (given an id, it can
be referred to later), and it is used by other elements. Written as a task it is just a data type declaration, which is
legal and useless. What a build author sees when something is wrong:
<target name="err-undefined">
<condition property="x"><nosuchcondition/></condition>
</target>
err-undefined:
BUILD FAILED
<scratch>/build.xml:115: condition doesn't support the nested "nosuchcondition" element.
The same message appears when the name exists but the class is not a condition. A typedef can succeed for a class that
implements none of the interfaces Ant expects, and the complaint only comes at the place of use:
err-wrong-kind: (typedef name="notacondition" of StationFileMapper, then used as a condition)
BUILD FAILED
<scratch>/build.xml:129: condition doesn't support the nested "notacondition" element.
A class that cannot be found is reported by the typedef itself, with the class loader that looked for it (useful when
several are in play), and onerror="report" turns the failure into a warning:
err-missing-class:
BUILD FAILED
<scratch>/build.xml:119: typedef class io.tidepool.ant.types.Ghost cannot be found
using the classloader AntClassLoader[<scratch>/build/classes]
err-onerror:
[typedef] <scratch>/build.xml:123: Warning: typedef class io.tidepool.ant.types.Ghost cannot be found
[typedef] using the classloader AntClassLoader[<scratch>/build/classes]
[echo] the build goes on
Use onerror="report" or ignore for optional extensions: for example a build that uses a custom selector only when a
jar is on the classpath. For anything required the default fail is right, because a misspelled classpath otherwise turns
into the confusing "doesn’t support the nested" errors above.
Source: Apache Ant manual: Typedef
Redefining a name
A second typedef of the same name replaces the first, with a message at the default level. This is occasionally
what is wanted (the build overrides a type of an imported file) and often a bug. After the second typedef below, mine is
the selector and no longer a condition:
typedef-override:
Trying to override old definition of datatype mine
[echo] stations.csv
BUILD FAILED
<scratch>/build.xml:149: condition doesn't support the nested "mine" element.
componentdef: the same name for different kinds of component
componentdef is a typedef with two differences: the definition is only visible to elements that accept components through
an add method, never as a task or a top-level data type, and several components may share one name provided they
implement different interfaces. Ant uses it for its own and, or and not, which exist as conditions and as selectors
(and as resource selectors). Two components named has, one condition and one selector:
<target name="componentdef">
<componentdef name="has" classname="io.tidepool.ant.types.StationIdCondition" classpath="${classes}"/>
<componentdef name="has" classname="io.tidepool.ant.types.CsvRowsSelector" classpath="${classes}"/>
<condition property="known" value="yes" else="no"><has id="HFX002" file="data/stations.csv"/></condition>
<fileset id="wide" dir="data"><has min="2"/></fileset>
<echo message="condition: ${known}; selector: ${toString:wide}"/>
</target>
componentdef:
[echo] condition: yes; selector: stations.csv
Inside condition Ant picked the condition class and inside fileset the selector class, by the interface the container
accepts. Used as a task, a componentdef name does not exist at all:
componentdef-toplevel:
BUILD FAILED
<scratch>/build.xml:176: Problem: failed to create task or type has
Cause: The name is undefined.
Action: Check the spelling.
Action: Check that any custom tasks/types have been declared.
Action: Check that any <presetdef>/<macrodef> declarations have taken place.
Prefer typedef for your own types, with distinct names; reserve componentdef for the case of a name that deliberately means
different things in different places.
Source: Apache Ant manual: Componentdef
Class loaders: loaderref
Each typedef with a classpath creates its own class loader. Two definitions of classes that share Java code or static state
therefore see two unrelated copies of it: the classes are "the same name" but not the same class to the JVM. The class
below keeps a static counter and logs it; it is declared with typedef under two names, with the same classpath:
package io.tidepool.ant.types;
import org.apache.tools.ant.ProjectComponent;
import org.apache.tools.ant.taskdefs.condition.Condition;
/** Condition with static state: shows whether two typedefs share one class loader. */
public class CallCounterCondition extends ProjectComponent implements Condition {
private static int calls; // one copy per class loader
@Override
public boolean eval() {
calls++;
log("evaluation number " + calls, org.apache.tools.ant.Project.MSG_WARN);
return true;
}
}
<target name="loaders-separate">
<typedef name="count1" classname="io.tidepool.ant.types.CallCounterCondition" classpath="${classes}"/>
<typedef name="count2" classname="io.tidepool.ant.types.CallCounterCondition" classpath="${classes}"/>
<condition property="p"><count1/></condition>
<condition property="q"><count2/></condition>
<condition property="r"><count1/></condition>
</target>
<target name="loaders-shared">
<typedef name="count1" classname="io.tidepool.ant.types.CallCounterCondition" classpath="${classes}" loaderref="tidepool.loader"/>
<typedef name="count2" classname="io.tidepool.ant.types.CallCounterCondition" classpath="${classes}" loaderref="tidepool.loader"/>
<condition property="p"><count1/></condition>
<condition property="q"><count2/></condition>
<condition property="r"><count1/></condition>
</target>
loaders-separate:
evaluation number 1
evaluation number 1
evaluation number 2
loaders-shared:
evaluation number 1
evaluation number 2
evaluation number 3
The first target proves it: count2 started again at 1 although count1 had run, because it ran in a different class
loader. With loaderref both definitions share one loader, and the state is shared. The manual’s rules follow from this:
Definitions made with typedef that must cooperate through Java code (a type and the helper it creates) need either loaderref with identical
classpaths (same components in the same order) or, better, a single antlib descriptor loaded with resource, which uses one
loader for all the definitions it contains.
Custom conditions
A condition is a class with a public no-argument constructor and a method boolean eval() (the interface Condition).
Extending ProjectComponent as well gives access to the project and to log(), so that the condition can write
verbose messages and resolve things against the project. The one below is true for a station id of the form AAA999 and,
when a file is given, only if that CSV file lists the id. Attributes are setId and setFile; a File attribute is
resolved by Ant against the project’s base directory before the setter is called.
package io.tidepool.ant.types;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.regex.Pattern;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.ProjectComponent;
import org.apache.tools.ant.taskdefs.condition.Condition;
/** Condition: true when "id" has the form AAA999 and, if "file" is set, the id is listed in that CSV file. */
public class StationIdCondition extends ProjectComponent implements Condition {
private static final Pattern FORMAT = Pattern.compile("[A-Z]{3}[0-9]{3}");
private String id;
private File file;
public void setId(String id) { this.id = id; } // attribute "id"
public void setFile(File file) { this.file = file; } // attribute "file", resolved against the project base directory
@Override
public boolean eval() throws BuildException {
if (id == null) {
throw new BuildException("the id attribute is required");
}
if (!FORMAT.matcher(id).matches()) {
log("'" + id + "' does not look like a station id", org.apache.tools.ant.Project.MSG_VERBOSE);
return false;
}
if (file == null) {
return true;
}
try {
return Files.readAllLines(file.toPath(), StandardCharsets.UTF_8).stream()
.anyMatch(line -> line.startsWith(id + ","));
} catch (IOException e) {
throw new BuildException("cannot read " + file, e, getLocation());
}
}
}
<target name="conditions">
<condition property="a" value="true" else="false"><validstation id="LIS001"/></condition>
<condition property="b" value="true" else="false"><validstation id="LIS001" file="data/stations.csv"/></condition>
<condition property="c" value="true" else="false"><validstation id="XYZ999" file="data/stations.csv"/></condition>
<condition property="d" value="true" else="false"><validstation id="lisbon"/></condition>
<condition property="e" value="true" else="false"><not><validstation id="lisbon"/></not></condition>
<echo message="a=${a} b=${b} c=${c} d=${d} e=${e}"/>
<fail message="not a valid station"><condition><not><validstation id="SYD003" file="data/stations.csv"/></not></condition></fail>
</target>
conditions:
[echo] a=true b=true c=false d=false e=true
The not in the fifth line and the condition nested in fail show the "first class" property: the custom condition works
inside the core containers (and, or, not, xor) without any registration other than the typedef. The value/else
attributes of condition matter: without else, a false condition leaves the property undefined and ${c} would print
literally as ${c}, which is a classic surprise (a condition task sets the property only when true; see
Conditions & Control Flow).
With -v the log(...) call in the condition becomes visible, once per evaluation of the invalid id:
'lisbon' does not look like a station id
'lisbon' does not look like a station id
When the required attribute is missing the condition throws, and Ant adds the build file location:
err-no-attribute: (<validstation/> with no id)
BUILD FAILED
<scratch>/build.xml:133: the id attribute is required
Two guidelines: validate in eval(), not in the setters, because the setters run in the order the attributes were written,
before the others are known; and never swallow an I/O exception into false, because a condition that cannot answer should
fail the build, not silently pick a branch.
Custom selectors
A selector decides for each candidate file of a fileset: select it or not. There are three ways to write one, in
increasing order of age, and one more way to write a container of selectors. The interface is always the same method,
isSelected(File basedir, String filename, File file), where basedir is the base directory of the fileset, filename
the path relative to it and file the file itself. Two rules about the arguments are easy to forget: the scanner also offers
directories (the base directory included), and the selectors run after the patterns, in the order written, so a selector
is never called for a file the includes and excludes already rejected.
The new style: extend BaseSelector
A class that implements FileSelector and is declared with typedef is a selector. BaseSelector is the convenience base class
(it is a DataType, so refid and id work): setError(message) records a configuration problem, verifySettings() is the
hook in which the class checks its own configuration, and validate() raises a BuildException with the recorded message. Call
validate() at the top of isSelected. This selector selects .csv files with at least min data rows:
package io.tidepool.ant.types;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.types.selectors.BaseSelector;
/** Selector (new style, a datatype): selects .csv files with at least "min" data rows below the header. */
public class CsvRowsSelector extends BaseSelector {
private int min = -1;
public void setMin(int min) { this.min = min; }
@Override
public void verifySettings() { // called by validate()
if (min < 0) {
setError("the min attribute is required and must not be negative");
}
}
@Override
public boolean isSelected(File basedir, String filename, File file) {
validate(); // throws BuildException with the setError() message
if (!file.isFile() || !filename.endsWith(".csv")) {
return false;
}
try {
return Files.readAllLines(file.toPath(), StandardCharsets.UTF_8).size() - 1 >= min;
} catch (IOException e) {
throw new BuildException("cannot read " + file, e);
}
}
}
<target name="selectors">
<fileset id="wide" dir="data"><csvrows min="2"/></fileset>
<pathconvert property="wide.list" refid="wide" pathsep=", "><mapper type="flatten"/></pathconvert>
<echo message="csvrows min=2: ${wide.list}"/>
...
</target>
selectors:
[echo] csvrows min=2: stations.csv
The fixtures are stations.csv (three rows), short.csv (one row) and empty.csv (none), so only the first has at least
two rows. Forgetting the min attribute is reported through setError at the place of use:
selector-errors: (<csvrows/> with no min)
BUILD FAILED
<scratch>/build.xml:47: the min attribute is required and must not be negative
The old style: extend BaseExtendSelector and use custom
Before Ant 1.6 a custom selector was an ExtendFileSelector, configured with param elements and used through the
custom element with the class name; it is still supported and is the form to choose when the class should be usable without
any typedef (the class name is in the build file). BaseExtendSelector extends BaseSelector, so the error handling is
the same; it adds setParameters(Parameter...), which receives the param elements. A selector that selects the CSV files
whose first column header is a given value:
package io.tidepool.ant.types;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.List;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.types.Parameter;
import org.apache.tools.ant.types.selectors.BaseExtendSelector;
/** Selector (old style): used through <custom classname="..."> with nested <param name="header" value="..."/>. */
public class FirstColumnSelector extends BaseExtendSelector {
private String header;
@Override
public void setParameters(Parameter... parameters) {
super.setParameters(parameters);
for (Parameter p : parameters) {
if ("header".equalsIgnoreCase(p.getName())) {
header = p.getValue();
} else {
setError("unknown parameter " + p.getName());
}
}
}
@Override
public void verifySettings() {
if (header == null) {
setError("the header parameter is required");
}
}
@Override
public boolean isSelected(File basedir, String filename, File file) throws BuildException {
validate();
if (!file.isFile()) {
return false; // directories are offered to selectors too
}
try {
List<String> lines = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8);
return !lines.isEmpty() && lines.get(0).split(",")[0].equals(header);
} catch (IOException e) {
throw new BuildException("cannot read " + file, e);
}
}
}
<fileset id="byheader" dir="data">
<custom classname="io.tidepool.ant.types.FirstColumnSelector" classpath="${classes}">
<param name="header" value="id"/>
</custom>
</fileset>
<resourcecount refid="byheader" property="byheader.count"/>
<echo message="custom FirstColumnSelector header=id: ${byheader.count} files"/>
[echo] custom FirstColumnSelector header=id: 3 files
All three CSV files start with id. The custom element takes classname, classpath and classpathref, as in the table
of the manual. Some core selectors (contains, date, depth, filename, size) can also be used through custom with
their class names, which is how they were written before the new style existed.
The isFile() test at the top of isSelected is there because of a failure of the first version of this class, which had no
test. Ant offers the base directory itself to the selectors, and reading a directory fails:
selectors:
[echo] csvrows min=2: stations.csv
BUILD FAILED
<scratch>/build.xml:31: cannot read <scratch>/data
(CsvRowsSelector was spared because it already tested file.isFile() and the .csv suffix.) Every custom selector must
decide what to do with directories: return false, as here, for a selector about file contents, or decide on the name for one
about names.
Selector containers: extend BaseSelectorContainer
A container is a selector with nested selectors, like and or majority. BaseSelectorContainer provides all the addX
methods for the core selectors and the selectorElements() enumeration. The container below selects a file when exactly number of the
nested selectors select it, and the build file mixes core selectors and the custom csvrows inside it:
package io.tidepool.ant.types;
import java.io.File;
import java.util.Enumeration;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.types.selectors.BaseSelectorContainer;
import org.apache.tools.ant.types.selectors.FileSelector;
/** Selector container: selects a file when exactly "number" of the nested selectors select it. */
public class MatchNumberSelectors extends BaseSelectorContainer {
private int number = -1;
public void setNumber(int number) { this.number = number; }
@Override
public void verifySettings() {
if (number < 0) {
throw new BuildException("the number attribute must be set");
}
}
@Override
public boolean isSelected(File baseDir, String filename, File file) {
validate();
int selected = 0;
for (Enumeration<FileSelector> e = selectorElements(); e.hasMoreElements();) {
if (e.nextElement().isSelected(baseDir, filename, file)) {
selected++;
}
}
return selected == number;
}
}
<fileset id="two" dir="data">
<matchnumber number="2">
<filename name="s*"/>
<csvrows min="2"/>
<size value="10" when="more"/>
</matchnumber>
</fileset>
[echo] matchnumber number=2: short.csv
Only short.csv satisfies exactly two of the three: its name starts with s and it is larger than ten bytes, but it has one
data row, not two. stations.csv satisfies all three and empty.csv none. Without the number attribute verifySettings
throws its own exception, which is the other way to report a bad configuration (as opposed to setError):
err-matchnumber:
BUILD FAILED
<scratch>/build.xml:191: the number attribute must be set
The core selectors, the container selectors and the scriptselector that writes the same logic in a few lines of JavaScript
or Groovy are on the Selectors page; the script variant needs no
Java class and no compile step, but it is slower and is subject to the security caveats of scripts
(see Scripting).
Custom resource collections
A resource collection is the Ant 1.7 abstraction behind filesets, paths, archives and more: anything that yields
Resource objects. Implementing ResourceCollection (iterator(), size(), isFilesystemOnly()) and extending DataType gives a
type that every consumer of resources accepts, including copy (when the resources are files), concat, restrict, union
and resourcecount. The class below turns the station ids of a CSV file into StringResource objects, one per row. A
resource has a name and, for a string resource, a value that is also its name; the class would equally well produce
FileResource or URLResource objects.
package io.tidepool.ant.types;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.types.DataType;
import org.apache.tools.ant.types.Resource;
import org.apache.tools.ant.types.ResourceCollection;
import org.apache.tools.ant.types.resources.StringResource;
/** ResourceCollection: one StringResource per station id in a CSV file (its name and value are the id). */
public class StationIds extends DataType implements ResourceCollection {
private File file;
public void setFile(File file) {
checkAttributesAllowed(); // not allowed together with refid
this.file = file;
}
@Override
public Iterator<Resource> iterator() {
if (isReference()) {
return getCheckedRef(StationIds.class).iterator();
}
if (file == null) {
throw new BuildException("the file attribute is required");
}
try {
List<Resource> result = new ArrayList<>();
List<String> lines = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8);
for (String line : lines.subList(1, lines.size())) {
result.add(new StringResource(line.split(",")[0]));
}
return result.iterator();
} catch (IOException e) {
throw new BuildException("cannot read " + file, e, getLocation());
}
}
@Override public int size() {
int n = 0;
for (Iterator<Resource> i = iterator(); i.hasNext(); i.next()) { n++; }
return n;
}
@Override public boolean isFilesystemOnly() { return false; }
}
Three details of the class are what a collection needs to behave in a build. checkAttributesAllowed() makes the type reject
attributes when it is used through refid, as the core types do (<stationids refid="s1" file="..."/> fails with
"You must not specify more than one attribute when using refid"). The isReference() branch makes refid work by delegating
to the referenced object. And isFilesystemOnly() returns false, which tells consumers that need real files, such as tasks that
pass the paths to an external program, that these resources are not files.
<target name="resources">
<stationids id="stations" file="data/stations.csv"/>
<resourcecount refid="stations" property="station.count"/>
<echo message="stations: ${station.count}"/>
<pathconvert property="names" refid="stations" pathsep=" | "><mapper type="identity"/></pathconvert>
<echo message="names: ${names}"/>
<concat><stationids refid="stations"/></concat>
<restrict id="long">
<stationids file="data/stations.csv"/>
<name name="S*"/>
</restrict>
<resourcecount refid="long" property="s.count"/>
<echo message="stations starting with S: ${s.count}"/>
</target>
resources:
[echo] stations: 3
[echo] names: LIS001 | HFX002 | SYD003
[concat] LIS001HFX002SYD003
[echo] stations starting with S: 1
concat writes the values of the resources one after the other (without separators, as it would for files), which is why the
ids run together. copy accepts the collection too: <copy todir="build/x"><stationids file="data/stations.csv"/></copy> printed
Copying 3 resources to <scratch>/build/x and created the files LIS001, HFX002 and SYD003, each holding its own id, because a
string resource can be read like a file. restrict with the core resource selector name kept the one station whose id starts with S. A first
version of this class called setName on the StringResource, which fails: the name of a string resource is its value and
is immutable (the error is an ImmutableResourceException, reported without a message). The resource abstraction, the
core collections (union, sort, first, mappedresources …) and the resource selectors are covered in
Resource Collections.
Custom mappers
A mapper turns a source name into zero or more target names. FileNameMapper has three methods: setFrom and setTo
(the from and to attributes of the mapper element, which the implementation may ignore) and
mapFileName(String), which returns the mapped names or null when the mapper does not apply to that file. Returning null
is the signal for "skip this file", which is how a copy with a mapper copies only the files the mapper maps.
package io.tidepool.ant.types;
import org.apache.tools.ant.util.FileNameMapper;
/** Mapper: stations_2026.csv -> stations-2026.csv (underscores to hyphens, name lower-cased); from/to are not used. */
public class StationFileMapper implements FileNameMapper {
@Override public void setFrom(String from) { }
@Override public void setTo(String to) { }
@Override
public String[] mapFileName(String sourceFileName) {
if (sourceFileName == null || !sourceFileName.endsWith(".csv")) {
return null; // null = this mapper does not map the file
}
return new String[] {sourceFileName.toLowerCase().replace('_', '-')};
}
}
A custom mapper can be used in two ways. With the classname attribute of the mapper element, no typedef is needed; or after a
typedef, as an element in its own right, wherever a mapper is allowed:
<target name="mapper">
<copy todir="build/mapped">
<fileset dir="incoming"/>
<mapper classname="io.tidepool.ant.types.StationFileMapper" classpath="${classes}"/>
</copy>
<copy todir="build/mapped2">
<fileset dir="incoming"/>
<stationfiles/>
</copy>
<pathconvert property="mapped.list" pathsep=", "><fileset dir="build/mapped"/><mapper type="flatten"/></pathconvert>
<echo message="classname form: ${mapped.list}"/>
<pathconvert property="mapped2.list" pathsep=", "><fileset dir="build/mapped2"/><mapper type="flatten"/></pathconvert>
<echo message="typedef form: ${mapped2.list}"/>
</target>
mapper:
[copy] Copying 2 files to <scratch>/build/mapped
[copy] Copying 2 files to <scratch>/build/mapped2
[echo] classname form: stations-2026.csv, tides-north.csv
[echo] typedef form: stations-2026.csv, tides-north.csv
The incoming directory contains Stations_2026.csv, Tides_North.csv and readme.txt; the mapper returned null for the
text file, so copy copied two files, with the new names. The built-in mappers (glob, regexp, package, flatten,
chained, scriptmapper…) and how to combine them are on the
Filters & Mappers page; most renaming rules are a regexp or
chained mapper, and a Java class is justified only when the rule needs real code.
Source: Apache Ant manual: Mapper
Custom filters
Filters are stream transformers, in two levels. A filter reader works on characters: it implements ChainableReader, whose
single method Reader chain(Reader) returns a reader that filters the given one, and it is used in a filterchain either
through filterreader with a class name and param elements (old style), or after a typedef as an element of its own (new style).
A token filter works on tokens, by default lines: it extends TokenFilter.ChainableReaderFilter, overrides filter(String), and
is a nested element of tokenfilter. The manual’s examples are a reader that drops every second character and a filter that
appends the line number; the first is rarely what a build needs, so the examples here are closer to the tidepool data.
A line filter
NumberLines prefixes every line with its number. ChainableReaderFilter already implements the plumbing (chain, byLine);
only filter is written, and setSeparator is a normal attribute:
package io.tidepool.ant.types;
import org.apache.tools.ant.filters.TokenFilter;
/** Filter: prefixes each line with its line number. Works as a nested element of <tokenfilter>. */
public class NumberLines extends TokenFilter.ChainableReaderFilter {
private int lineNumber;
private String separator = ": ";
public void setSeparator(String separator) { this.separator = separator; }
@Override
public String filter(String line) {
lineNumber++;
return lineNumber + separator + line;
}
}
<target name="filters">
<copy file="data/stations.csv" todir="build/filtered">
<filterchain>
<tokenfilter><numberlines separator=" | "/></tokenfilter>
</filterchain>
</copy>
<concat><fileset file="build/filtered/stations.csv"/></concat>
...
</target>
[concat] 1 | id,name,lat,lon
[concat] 2 | LIS001,Lisbon,38.70,-9.14
[concat] 3 | HFX002,Halifax,44.65,-63.57
[concat] 4 | SYD003,Sydney,-33.87,151.21
The filter has state, the line counter, and the state outlives a file: with a fileset of two files in the same copy, the
numbers continued from one file to the next, because Ant builds the filter chain once and reuses the same filter instance:
[concat] 1: id,name
[concat] 2: BAD1,Broken
[concat] 3: id,name,lat,lon
[concat] 4: LIS001,Lisbon,38.70,-9.14
[concat] 5: HFX002,Halifax,44.65,-63.57
[concat] 6: SYD003,Sydney,-33.87,151.21
(short.csv, two lines, was copied first.) A filter whose state must restart per file has to reset it itself, for example when
it sees the first line, or the build should copy the files one at a time. The reader-level filter below does not have the problem:
chain returns a fresh reader for each file.
A reader filter in both styles
DropColumnReader extends BaseParamFilterReader, which provides the line-reading helper and the parameter handling, and
implements ChainableReader so that it can also be declared with typedef. It removes one column of a CSV file and can be configured with a
param (old style) or with a column attribute (new style):
package io.tidepool.ant.types;
import java.io.IOException;
import java.io.Reader;
import org.apache.tools.ant.filters.BaseParamFilterReader;
import org.apache.tools.ant.filters.ChainableReader;
import org.apache.tools.ant.types.Parameter;
/**
* Filter reader: removes one comma-separated column (0-based). Works in both styles:
* old, as a filterreader with a param element, and new, typedef'd and used directly with the column attribute.
*/
public class DropColumnReader extends BaseParamFilterReader implements ChainableReader {
private int column = -1;
private String pending = "";
private int pos;
public DropColumnReader() { super(); } // needed to instantiate by name
public DropColumnReader(Reader in) { super(in); }
public void setColumn(int column) { this.column = column; } // attribute of the typedef'd form
@Override
public Reader chain(Reader rdr) {
DropColumnReader r = new DropColumnReader(rdr);
r.column = column;
r.setInitialized(true);
return r;
}
private void init() {
if (!getInitialized()) {
Parameter[] parameters = getParameters(); // set when used as <filterreader classname="...">
if (parameters != null) {
for (Parameter p : parameters) {
if ("column".equals(p.getName())) {
column = Integer.parseInt(p.getValue());
}
}
}
setInitialized(true);
}
}
@Override
public int read() throws IOException {
init();
while (pos >= pending.length()) {
String line = readLine();
if (line == null) {
return -1;
}
line = line.replaceAll("[\r\n]+$", ""); // readLine() keeps the line terminator
String[] cells = line.split(",", -1);
StringBuilder out = new StringBuilder();
for (int i = 0; i < cells.length; i++) {
if (i != column) {
if (out.length() > 0) { out.append(','); }
out.append(cells[i]);
}
}
pending = out.append('\n').toString();
pos = 0;
}
return pending.charAt(pos++);
}
}
<copy file="data/stations.csv" tofile="build/filtered/no-coordinates.csv">
<filterchain>
<filterreader classname="io.tidepool.ant.types.DropColumnReader" classpath="${classes}">
<param name="column" value="2"/>
</filterreader>
</filterchain>
</copy>
<typedef name="dropcolumn" classname="io.tidepool.ant.types.DropColumnReader" classpath="${classes}"/>
<copy file="data/stations.csv" tofile="build/filtered/typedef-form.csv">
<filterchain><dropcolumn column="3"/></filterchain>
</copy>
[concat] id,name,lon
[concat] LIS001,Lisbon,-9.14
[concat] HFX002,Halifax,-63.57
[concat] SYD003,Sydney,151.21
[concat] id,name,lat
[concat] LIS001,Lisbon,38.70
[concat] HFX002,Halifax,44.65
[concat] SYD003,Sydney,-33.87
The first file lost column 2 (lat, counting from 0) through the filterreader/param form, the second lost column 3 (lon) through the
typedef form. Two details cost time while writing the class. readLine() of BaseFilterReader returns the line with its
terminator, so a version that did not strip it produced a blank line after every row (visible in the first
run as empty [concat] lines). And getInitialized()/setInitialized(true) of the base class are the way to run the parameter
handling once; the chained copy returned by chain must be marked initialised, or it reads parameters that only the prototype has.
The same filter used on two files in one copy (a fileset of stations.csv and short.csv, column 1 dropped) handled each file
from its own first line:
[concat] id
[concat] BAD1
[concat] id,lat,lon
[concat] LIS001,38.70,-9.14
[concat] HFX002,44.65,-63.57
[concat] SYD003,-33.87,151.21
The core filters that cover most needs, and scriptfilter for the quick one-off, are in
Filters & Mappers.
PropertyHelper delegates
Every ${...} expansion and every property lookup goes through the project’s PropertyHelper, which consults a list of
delegates. The delegate interfaces are sub-interfaces of the marker interface PropertyHelper.Delegate: a
PropertyEvaluator resolves a name to a value (the toString: prefix of the previous examples is one), a
PropertySetter and a PropertyExpander change how properties are stored and expanded. This is advanced Ant; the manual
says it assumes familiarity with the modern Ant APIs. The delegate below resolves ${station:ID} to a display name and returns
null for any other name, which is the contract for "not mine, ask the next delegate":
package io.tidepool.ant.types;
import org.apache.tools.ant.PropertyHelper;
/** PropertyEvaluator delegate: resolves ${station:LIS001} to a display name; returns null for everything else. */
public class StationEvaluator implements PropertyHelper.PropertyEvaluator {
@Override
public Object evaluate(String property, PropertyHelper helper) {
if (!property.startsWith("station:")) {
return null; // not ours: PropertyHelper asks the next delegate
}
String id = property.substring("station:".length());
switch (id) {
case "LIS001": return "Lisbon";
case "HFX002": return "Halifax";
default: return "unknown station " + id;
}
}
}
The delegate is registered with the propertyhelper task, either as a nested element after a componentdef, or by reference
with the generic delegate element, as here (an object of a typedef type with an id is created first):
<target name="property-helper">
<typedef name="stationevaluator" classname="io.tidepool.ant.types.StationEvaluator" classpath="${classes}"/>
<stationevaluator id="evaluator"/>
<propertyhelper>
<delegate refid="evaluator"/>
</propertyhelper>
<echo message="${station:LIS001} / ${station:HFX002} / ${station:QQQ000}"/>
<property name="label" value="Port of ${station:LIS001}"/>
<echo message="${label}"/>
</target>
property-helper:
[echo] Lisbon / Halifax / unknown station QQQ000
[echo] Port of Lisbon
Delegates are consulted in last-in-first-out order, so a delegate added later is asked before the built-in ones, which is what
lets it take over a prefix. Resolve cheap names only: the evaluator runs for every single ${...} in the build.
Testing custom types
A type is easiest to test with the tool that ships for it: a build file that uses it and fails when the result is wrong. The
conditions target above is already that, with a fail; the selectors, mapper and resources targets can be turned into checks
by replacing the echo with a fail that has a condition on the expected value. AntUnit (version 1.5.0, an antlib) wraps this
into assertion tasks and a test runner for build files, and Ant’s own BuildFileRule (in ant-testutil.jar) runs a build file from a
JUnit test; both are described, together with the tidepool <checkstations> task, in
Writing Custom Tasks. A Java unit test of a type can instantiate
the class directly with a Project object, which is also how a condition’s eval() can be called with no build file at all.
The same ideas in Maven and Gradle
| Ant extension | Maven | Gradle |
|---|---|---|
custom condition |
a Maven plugin (a Mojo with a parameter) or a profile activation property; an |
a plain Kotlin or Groovy expression in the build script, or a |
custom selector |
|
a |
resource collection |
none; plugins read their own input |
a |
mapper |
|
|
filter reader |
resource filtering with |
|
property delegate |
none in the core; the |
|
The mapping principle is the one from the other Ant pages: Ant’s types are classes you declare and register, because XML is not a programming language; in Gradle the build script is the programming language, so the same extension is a closure or a lambda, and in Maven it is a plugin. Moving an Ant build with many custom types to either tool is covered in Migrating Between Tools; keeping them and calling Ant from Gradle is shown in Ant Integration.
Classic mistakes
-
Wrong or missing classpath. The typedef itself succeeds only if the class loads; with
onerror="report"the failure is a warning and every later use fails with "doesn’t support the nested" or "The name is undefined". -
A false
conditionleaves the property undefined. Usevalueandelse, or test withisset. -
A selector that does not handle directories. The base directory and sub-directories are offered to
isSelected. -
State in a static field. It is shared by everything loaded by the same class loader, which can outlive one build in an embedding program.
-
Two definitions of classes that share helpers, made without
loaderref. Each gets its own copy of every class, soinstanceof, casts and statics fail in ways that look impossible. -
setNameon aStringResource. Resources of this kind are immutable; use the constructor that takes the value. -
readLine()keeping the line break in a filter reader. Strip it before splitting the line. -
Doing work in the constructor or setters. Ant creates the object before the attributes are set and calls the setters in the order written; put the work and the validation in
eval(),isSelectedanditerator(). -
Per-file state in a filter. One filter instance serves all the files of a
copy; see the numbering example above. -
Redefinition by accident. A second
typedefof a name replaces the first, with only a message at the default level.