It is very easy to write your own task:
org.apache.tools.ant.Task
      or another class that was designed to be extended.public void method that takes a single argument. The
    name of the method must begin with set, followed by the
    attribute name, with the first character of the name in uppercase, and the rest in
    lowercase*.  That is, to support an attribute named
    file you create a method setFile.
    Depending on the type of the argument, Ant will perform some
    conversions for you, see below.parallel), your
    class must implement the interface
    org.apache.tools.ant.TaskContainer.  If you do so, your
    task can not support any other nested elements.  See 
    below.public void addText(String)
    method.  Note that Ant does not expand properties on
    the text it passes to the task.public method that takes no arguments and returns an
    Object type. The name of the create method must begin
    with create, followed by the element name. An add (or
    addConfigured) method must be a public void method that
    takes a single argument of an Object type with a
    no-argument constructor.  The name of the add (addConfigured) method
    must begin with add (addConfigured),
    followed by the element name.  For a more complete discussion see 
    below.public void execute method, with no arguments, that
    throws a BuildException. This method implements the task
    itself.* Actually the case of the letters after the first one doesn't really matter to Ant, using all lower case is a good convention, though.
perform() method. This instantiates
    the task. This means that tasks only gets
    instantiated at run time.
  project and
    location variables.id attribute to this task,
    the project
    registers a reference to this newly created task, at run
    time.target variable.init() is called at run time.createXXX() methods or
    instantiated and added to this task via its addXXX()
    methods, at run time.  Child elements corresponding
    to addConfiguredXXX() are created at this point but
    the actual addConfigured method is not called.setXXX methods, at runtime.addText method, at runtime.setXXX methods, at runtime.addConfiguredXXX() methods,
    those methods get invoked now.execute() is called at runtime.
    If target1 and target2 both depend
    on target3, then running
    'ant target1 target2' will run all tasks in
    target3 twice.Ant will always expand properties before it passes the value of an attribute to the corresponding setter method. Since Ant 1.8, it is possible to extend Ant's property handling such that a non-string Object may be the result of the evaluation of a string containing a single property reference. These will be assigned directly via setter methods of matching type. Since it requires some beyond-the-basics intervention to enable this behavior, it may be a good idea to flag attributes intended to permit this usage paradigm.
The most common way to write an attribute setter is to use a
java.lang.String argument.  In this case Ant will pass
the literal value (after property expansion) to your task.  But there
is more!  If the argument of you setter method is
boolean, your method will be passed the value
  true if the value specified in the build file is one of
  true, yes, or on and
  false otherwise.char or java.lang.Character, your
  method will be passed the first character of the value specified in
  the build file.int, short
  and so on), Ant will convert the value of the attribute into this
  type, thus making sure that you'll never receive input that is not a
  number for that attribute.java.io.File, Ant will first determine whether the
  value given in the build file represents an absolute path name.  If
  not, Ant will interpret the value as a path name relative to the
  project's basedir.org.apache.tools.ant.types.Resource
  org.apache.tools.ant.types.Resource, Ant will
  resolve the string as a java.io.File as above, then
  pass in as a org.apache.tools.ant.types.resources.FileResource.
  Since Ant 1.8
  org.apache.tools.ant.types.Path, Ant will tokenize
  the value specified in the build file, accepting : and
  ; as path separators.  Relative path names will be
  interpreted as relative to the project's basedir.java.lang.Class, Ant will interpret the value
  given in the build file as a Java class name and load the named
  class from the system class loader.String argument, Ant will use this constructor to
  create a new instance from the value given in the build file.org.apache.tools.ant.types.EnumeratedAttribute, Ant
  will invoke this classes setValue method.  Use this if
  your task should support enumerated attributes (attributes with
  values that must be part of a predefined set of values).  See
  org/apache/tools/ant/taskdefs/FixCRLF.java and the
  inner AddAsisRemove class used in setCr
  for an example.EnumeratedAttribute and can result in cleaner code, but of course
  your task will not run on JDK 1.4 or earlier. Note that any override of
  toString() in the enumeration is ignored; the build file must use
  the declared name (see Enum.getName()). You may wish to use lowercase
  enum constant names, in contrast to usual Java style, to look better in build files.
  As of Ant 1.7.0.What happens if more than one setter method is present for a given
attribute?  A method taking a String argument will always
lose against the more specific methods.  If there are still more
setters Ant could chose from, only one of them will be called, but we
don't know which, this depends on the implementation of your Java
virtual machine.
Let's assume your task shall support nested elements with the name
inner.  First of all, you need a class that represents
this nested element.  Often you simply want to use one of Ant's
classes like org.apache.tools.ant.types.FileSet to
support nested fileset elements.
Attributes of the nested elements or nested child elements of them will be handled using the same mechanism used for tasks (i.e. setter methods for attributes, addText for nested text and create/add/addConfigured methods for child elements).
Now you have a class NestedElement that is supposed to
be used for your nested <inner> elements, you have
three options:
public NestedElement createInner()public void addInner(NestedElement anInner)public void addConfiguredInner(NestedElement anInner)What is the difference?
Option 1 makes the task create the instance of
NestedElement, there are no restrictions on the type.
For the options 2 and 3, Ant has to create an instance of
NestedInner before it can pass it to the task, this
means, NestedInner must have a public no-arg
  constructor or a public one-arg constructor
  taking a Project class as a parameter.
This is the only difference between options 1 and 2.
The difference between 2 and 3 is what Ant has done to the object
before it passes it to the method.  addInner will receive
an object directly after the constructor has been called, while
addConfiguredInner gets the object after the
attributes and nested children for this new object have been
handled.
What happens if you use more than one of the options? Only one of the methods will be called, but we don't know which, this depends on the implementation of your Java virtual machine.
<typedef> you have two options.
  public void add(Type type)public void addConfigured(Type type)For example suppose one wanted to handle objects object of type org.apache.tools.ant.taskdefs.condition.Condition, one may have a class:
  
public class MyTask extends Task {
    private List conditions = new ArrayList();
    public void add(Condition c) {
        conditions.add(c);
    }
    public void execute() {
     // iterator over the conditions
    }
}
  
  
  One may define and use this class like this:
    
<taskdef name="mytask" classname="MyTask" classpath="classes"/>
<typedef name="condition.equals"
         classname="org.apache.tools.ant.taskdefs.conditions.Equals"/>
<mytask>
    <condition.equals arg1="${debug}" arg2="true"/>
</mytask>
    
  
  A more complicated example follows:
    
public class Sample {
    public static class MyFileSelector implements FileSelector {
         public void setAttrA(int a) {}
         public void setAttrB(int b) {}
         public void add(Path path) {}
         public boolean isSelected(File basedir, String filename, File file) {
             return true;
         }
     }
    interface MyInterface {
        void setVerbose(boolean val);
    }        
    public static class BuildPath extends Path {
        public BuildPath(Project project) {
            super(project);
        }
        
        public void add(MyInterface inter) {}
        public void setUrl(String url) {}
    }
    public static class XInterface implements MyInterface {
        public void setVerbose(boolean x) {}
        public void setCount(int c) {}
    }
}
    
  
  This class defines a number of static classes that implement/extend Path, MyFileSelector and MyInterface. These may be defined and used as follows:
    
<typedef name="myfileselector" classname="Sample$MyFileSelector"
         classpath="classes" loaderref="classes"/>
<typedef name="buildpath" classname="Sample$BuildPath"
         classpath="classes" loaderref="classes"/>
<typedef name="xinterface" classname="Sample$XInterface"
         classpath="classes" loaderref="classes"/>
<copy todir="copy-classes">
   <fileset dir="classes">
      <myfileselector attra="10" attrB="-10">
         <buildpath path="." url="abc">
            <xinterface count="4"/>
         </buildpath>
      </myfileselector>
   </fileset>
</copy>
    
  
The TaskContainer consists of a single method,
addTask that basically is the same as an add method for nested elements.  The task
instances will be configured (their attributes and nested elements
have been handled) when your task's execute method gets
invoked, but not before that.
When we said execute would be
called, we lied ;-).  In fact, Ant will call the perform
method in org.apache.tools.ant.Task, which in turn calls
execute.  This method makes sure that Build Events will be triggered.  If you
execute the task instances nested into your task, you should also
invoke perform on these instances instead of
execute.
Let's write our own task, which prints a message on the
System.out stream.
The task has one attribute, called message.
package com.mydomain;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
public class MyVeryOwnTask extends Task {
    private String msg;
    // The method executing the task
    public void execute() throws BuildException {
        System.out.println(msg);
    }
    // The setter for the "message" attribute
    public void setMessage(String msg) {
        this.msg = msg;
    }
}
It's really this simple ;-)
Adding your task to the system is rather simple too:
<taskdef> element to your project.
    This actually adds your task to the system.
<?xml version="1.0"?>
<project name="OwnTaskExample" default="main" basedir=".">
  <taskdef name="mytask" classname="com.mydomain.MyVeryOwnTask"/>
  <target name="main">
    <mytask message="Hello World! MyVeryOwnTask works!"/>
  </target>
</project>
<taskdef> declaration inside a target
after the compilation. Use the classpath attribute of
<taskdef> to point to where the code has just been
compiled.
<?xml version="1.0"?>
<project name="OwnTaskExample2" default="main" basedir=".">
  <target name="build" >
    <mkdir dir="build"/>
    <javac srcdir="source" destdir="build"/>
  </target>
  <target name="declare" depends="build">
    <taskdef name="mytask"
        classname="com.mydomain.MyVeryOwnTask"
        classpath="build"/>
  </target>
  <target name="main" depends="declare">
    <mytask message="Hello World! MyVeryOwnTask works!"/>
  </target>
</project>
Another way to add a task (more permanently), is to add the task name and
implementing class name to the default.properties file in the
org.apache.tools.ant.taskdefs
package. Then you can use it as if it were a built-in task.
Ant is capable of generating build events as it performs the tasks necessary to build a project. Listeners can be attached to Ant to receive these events. This capability could be used, for example, to connect Ant to a GUI or to integrate Ant with an IDE.
To use build events you need to create an ant Project object. You can then call the
addBuildListener method to add your listener to the project. Your listener must implement
the org.apache.tools.antBuildListener interface. The listener will receive BuildEvents
for the following events
If the build file invokes another build file via 
<ant> or 
<subant> or uses 
<antcall>, you are creating a
new Ant "project" that will send target and task level events of its
own but never sends build started/finished events.  Ant 1.6.2
introduces an extension of the BuildListener interface named
SubBuildListener that will receive two new events for
If you are interested in those events, all you need to do is to implement the new interface instead of BuildListener (and register the listener, of course).
If you wish to attach a listener from the command line you may use the
-listener option. For example:
ant -listener org.apache.tools.ant.XmlLogger
will run Ant with a listener that generates an XML representation of the build progress. This listener is included with Ant, as is the default listener, which generates the logging to standard output.
Note: A listener must not access System.out and System.err directly since output on these streams is redirected by Ant's core to the build event system. Accessing these streams can cause an infinite loop in Ant. Depending on the version of Ant, this will either cause the build to terminate or the Java VM to run out of Stack space. A logger, also, may not access System.out and System.err directly. It must use the streams with which it has been configured.
Note2: All methods of a BuildListener except for the "Build
  Started" and "Build Finished" events may occur on several threads
  simultaneously - for example while Ant is executing
  a <parallel> task.
-listener option as
described above.
public class MyLogAdapter implements BuildListener {
    private MyLogger getLogger() {
        final MyLogger log = MyLoggerFactory.getLogger(Project.class.getName());
        return log;
    }
    @Override
    public void buildStarted(final BuildEvent event) {
        final MyLogger log = getLogger();
        log.info("Build started.");
    }
    @Override
    public void buildFinished(final BuildEvent event) {
        final MyLogger logger = getLogger();
        MyLogLevelEnum loglevel = ... // map event.getPriority() to enum via Project.MSG_* constants
        boolean allOK = event.getException() == null;
        String logmessage = ... // create log message using data of the event and the message invoked
        logger.log(loglevel, logmessage);
    }
    // implement all methods in that way
}
The other way to extend Ant through Java is to make changes to existing tasks, which is positively encouraged. Both changes to the existing source and new tasks can be incorporated back into the Ant codebase, which benefits all users and spreads the maintenance load around.
Please consult the Getting Involved pages on the Apache web site for details on how to fetch the latest source and how to submit changes for reincorporation into the source tree.
Ant also has some task guidelines which provides some advice to people developing and testing tasks. Even if you intend to keep your tasks to yourself, you should still read this as it should be informative.