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

org.apache.tools.ant.taskdefs.condition.Condition (boolean eval())

condition, fail, waitfor, and inside and, or, not

a selector

org.apache.tools.ant.types.selectors.FileSelector, usually through BaseSelector (new style) or BaseExtendSelector (old style, custom)

nested in a fileset, dirset, or in the fileset of copy, delete and other tasks

a selector container

BaseSelectorContainer

a logic of your own over nested selectors

a resource collection

org.apache.tools.ant.types.ResourceCollection, usually extending DataType

copy, concat, restrict, union, pathconvert, resourcecount

a mapper

org.apache.tools.ant.util.FileNameMapper

mapper, copy, move, uptodate, apply

a filter

ChainableReader (reader level) or TokenFilter.ChainableReaderFilter (line level)

filterchain, tokenfilter

a property resolver

PropertyHelper.PropertyEvaluator (or another Delegate sub-interface)

${prefix:name} expressions, installed with propertyhelper

flowchart TD q{"What does the new element answer?"} q -->|"true or false"| cond["Condition: eval()"] q -->|"is this file in?"| sel["FileSelector: isSelected(basedir, name, file)"] q -->|"which resources?"| rc["ResourceCollection: iterator(), size()"] q -->|"what is the target name?"| map["FileNameMapper: mapFileName(name)"] q -->|"how is the text changed?"| flt["ChainableReader: chain(reader)"] q -->|"what is this property?"| pe["PropertyEvaluator: evaluate(name, helper)"] cond --> td["typedef name + classname + classpath"] sel --> td rc --> td map --> td flt --> td pe --> td td --> use["use the element wherever the core element is allowed"]

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

name

the element name; required unless file or resource is used

classname

the class implementing the type

file, resource

load many definitions at once: file is a path, resource is looked up on the classpath. If several resources have that name and the format is properties, the first is used, otherwise all are loaded

format

properties (lines of name=classname) or xml (an antlib descriptor); the default is properties unless the name ends in .xml (since Ant 1.6)

classpath, classpathref

where to find the class

loaderref

the name of a class loader to create from the classpath and share between definitions (since Ant 1.5)

onerror

what to do if the definition fails: fail (the default since 1.7), report (warn and go on), ignore, and failall (also fail when a file or resource is missing; since 1.6)

adapter, adaptto

an adapter class (implementing TypeAdapter) that wraps the defined class to make it a task or another type; with adaptto, only when the class is not already of that type (since 1.6)

uri

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.

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.

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.

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.

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 enforcer custom rule is the closest to a "fail when false" condition (see Enforcer)

a plain Kotlin or Groovy expression in the build script, or a Spec/Provider with onlyIf and enabled

custom selector

includes/excludes patterns of the plugin; nothing evaluates a predicate over file contents or attributes in the core

a Spec<FileTreeElement> in include/exclude, or a filtered FileTree; the predicate is a lambda

resource collection

none; plugins read their own input

a FileCollection/FileTree/Provider built from any source in a few lines

mapper

<resources> plus a rename in the assembly or war descriptors; the antrun plugin runs an Ant mapper

rename, eachFile { } and rename(regex, replacement) in Copy/Sync/archive tasks

filter reader

resource filtering with ${...} and @...@ delimiters; the filtering of the resources plugin

filter(...) and expand(...) on Copy, which take a closure, a class or a map of properties

property delegate

none in the core; the properties plugin and settings or profiles supply values

providers, gradle.properties and ext properties; a ValueSource for computed values

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 condition leaves the property undefined. Use value and else, or test with isset.

  • 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, so instanceof, casts and statics fail in ways that look impossible.

  • setName on a StringResource. 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(), isSelected and iterator().

  • 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 typedef of a name replaces the first, with only a message at the default level.