Tải bản đầy đủ
7 Handling Ant’s input and output

7 Handling Ant’s input and output

Tải bản đầy đủ

Figure 18.1 The BuildListener and BuildLogger receive lifecycle
events, events which are described by BuildEvent objects.

A BuildListener is a Java class that receives notifications of various build, target,
and task lifecycle events during a build. The events are build started/finished, target
started/finished, task started/finished, and message logged. A project can have any
number of build listeners. Ant internally attaches some of its own build listeners to
catch events, particularly the build-finished event, which triggers cleanups. Each of
the events is handed a BuildEvent instance. This BuildEvent encapsulates all the
details of the event being triggered, as listed in table 18.2.
Table 18.2 The different BuildListener callbacks and the BuildEvent data they can
expect. For all the finished events, a non-null exception inside the BuildEvent implies that
the task, target, or build failed.

BuildListener event

BuildEvent contents

buildStarted

Project

buildFinished

Project and possibly an exception

targetStarted

Project and target

targetFinished

Project, target, and possibly exception

taskStarted

Project, target, and task
continued on next page

504

CHAPTER 18

EXTENDING ANT FURTHER

Table 18.2 The different BuildListener callbacks and the BuildEvent data they can
expect. For all the finished events, a non-null exception inside the BuildEvent implies that
the task, target, or build failed. (continued)

BuildListener event

BuildEvent contents

taskFinished

Project, target, task, and possibly exception.

messageLogged

Message and its priority. The Project attribute is always set, and
depending on where the message originated, the target and task
attributes may also be set.

The BuildLogger interface builds on its parent BuildListener by adding access
to the output and error print streams. Two additional methods that the BuildLogger
interface extends beyond BuildListener allow for setting the emacs mode and
the message output level. The DefaultLogger reacts to the emacs switch by generating output formatted for IDE integration, as its formatting of error locations in
files is something most IDEs can parse. The message output level is used to filter the
output based on the logging level.
Every Ant project has one and only one logger, which is hooked up to the output
streams and attached to the project as a listener to receive lifecycle events. Users can select
a specific logger on the command line via the -logger switch; otherwise they get the
default one. Using -emacs enables the emacs mode. The -quiet, -verbose, and
-debug switches can move the logging level up or down from its default of “info.”
Because only one logger is allowed, IDEs don’t let you switch from their custom loggers, which are needed to integrate Ant with the editor.
18.7.1

Writing a custom listener
As we stated, a listener is a Java class that implements BuildListener. Listing 18.7
shows a custom listener we’ve pulled together. It records the frequency of start and finish events and then, when the build finishes, prints them to System.out.
Listing 18.7

A listener that collects statistics

package org.antbook.listeners;
import org.apache.tools.ant.BuildListener;
import org.apache.tools.ant.BuildEvent;
public class StatsListener implements BuildListener {
private Tracker builds = new Tracker("Builds");
private Tracker targets = new Tracker("Targets");
private Tracker tasks = new Tracker("Tasks");
public void buildStarted(BuildEvent event) {
builds.enter(event);
}
public void buildFinished(BuildEvent event) {
builds.exit(event);
System.out.println(builds);

HANDLING ANT’S INPUT AND OUTPUT

These log
events

The build
has started
The build
has finished

505

System.out.println(targets);
System.out.println(tasks);

Print the
results

}

public void targetStarted(BuildEvent event) {
targets.enter(event);
}

A target has
started

public void targetFinished(BuildEvent event) {
targets.exit(event);
}
public void taskStarted(BuildEvent event) {
tasks.enter(event);
}

A target has
finished

A task has
started

public void taskFinished(BuildEvent event) {
tasks.exit(event);
}

A task has
finished

public void messageLogged(BuildEvent event) {
}

Ignore logged
messages

}

This listener delegates most of the work to a helper class—the Tracker class—
which tracks notifications. This class tracks the start and finish events for each category, as well as the last exception thrown on a failure:
package org.antbook.listeners;
import org.apache.tools.ant.BuildEvent;
public class Tracker {
private String category;
private volatile int count;
private volatile int depth;
private Throwable thrown;

What is
tracked

The total
entry count

Any exception thrown

public Tracker(String category) {
this.category = category;
}

Store the category
during creation

public synchronized void enter(BuildEvent event) {
count++;
depth++;
}
public synchronized void exit(BuildEvent event) {
depth--;
if(event.getException()!=null) {
thrown=event.getException();
}
}

506

The current
depth

CHAPTER 18

Entry: increase
count and depth

Decrease the
depth and cache
any exception

EXTENDING ANT FURTHER

public String toString() {
StringBuffer state =new StringBuffer();
state.append(category);
state.append("=");
state.append(count);
if(depth>0) {
state.append(" depth=");
state.append(depth);
}
if (thrown !=null) {
state.append(" (failure)");
}
return state.toString();
}

Construct and return
a string containing
everything that has
been recorded

}

This class’s toString() method prints the statistics for that particular category.
When a StatsListener instance receives a buildFinished() notification, it
prints out all the trackers’ statistics at that point. Assuming that we receive such a message at the end of the build, this should give the statistics of the build.
There’s no test suite for listeners and loggers, no equivalent to AntUnit. Ant’s
original test-harness JAR can be used to run Ant from JUnit tests, setting up a project
and making JUnit assertions about the results. Suspiciously, there are no tests for any
of Ant’s own listeners or loggers, just a MockBuildListener that appears in notification dispatch tests.
We’ll break our test-first rule and mimic the Ant team by running our code,
instead of rigorously testing it. To run our listener, all we need to do is run Ant from
the command line with the -listener argument pointing to the new class and the
newly created JAR appearing on the classpath:
ant -listener org.antbook.listeners.StatsListener \
-lib dist/antbook-log-1.0.jar dist
Buildfile: build.xml
init:
compile:
jar:
dist:
ready-to-run:
ready-to-install:
install:
installed:

HANDLING ANT’S INPUT AND OUTPUT

507

default:
BUILD SUCCESSFUL
Total time: 0 seconds
Builds=1
Targets=9
Tasks=16

This is actually mildly interesting, especially on a big project. A quick check of a
work-related build showed 596 targets and 3456 tasks. The project count remains at
one, even when multiple calls have invoked other build files. Listeners are
notified only on the big builds starting and finishing, not on subsidiary projects. It is,
however, possible to start targets and tasks before the previous one finishes, using
, , and the like.
What happens on a failing build? For that, we need a build file that fails, such as
that in listing 18.8.
Listing 18.8

A build file that always fails. The success and failure messages are
for the logger in section 18.7.2.


value="Well done, ${user.name}"/>
value="Better luck next time, ${user.name}"/>





Running the test now gives us a failure message:
ant -listener org.antbook.listeners.StatsListener \
-lib dist/antbook-log-1.0.jar -f test/fail.xml
Buildfile: test/fail.xml
fail:
BUILD FAILED
/home/ant/listeners/test/fail.xml:7: Example build failure
Total time: 0 seconds
builds=1 (failure)
targets=1 (failure)
tasks=3 (failure)

This build file shows that when a task fails, the containing target and build file also are
notified. There’s one more experiment to do: use the Ant-contrib task
508

CHAPTER 18

EXTENDING ANT FURTHER

of chapter 9, which can catch and discard exceptions, and see what the statistics are if
a task’s failure is caught and ignored:
Buildfile: test/nofail.xml
nofail:
BUILD SUCCESSFUL
Total time: 0 seconds
builds=1
targets=1
tasks=6 (failure)

This shows something mildly useful. If a task fails, it is signalled as such to a listener,
even if an outer container catches the failure and discards the exception.
18.7.2

Writing a custom logger
The next coding exercise after a custom listener is a custom logger, which is a class
that implements BuildLogger. This is simply a BuildListener with four additional methods to handle the system output and error streams as well as the setting of
some output options. The easiest way to do some logging, if all you want to do is
slightly tweak the normal output, is to extend Ant’s DefaultLogger class, which
is Ant’s normal logger. Listing 18.9 is an example of this.
Listing 18.9

A new logger, an extension of the normal one, that replaces the
normal success/failure message with one from project properties

package org.antbook.listeners;
import org.apache.tools.ant.DefaultLogger;
import org.apache.tools.ant.BuildEvent;
import org.apache.tools.ant.Project;
public class PersonalLogger extends DefaultLogger {
private static final String SUCCESS = "ant.build.success";
private static final String FAILURE = "ant.build.failure";
private String result;
@Override
public void buildFinished(BuildEvent event) {
String name;
name = event.getException() == null ?
SUCCESS
Select the property
:FAILURE;
Project p = event.getProject();
Read it
result = p.replaceProperties("${" + name + '}');
Expand it
super.buildFinished(event);
}
@Override

HANDLING ANT’S INPUT AND OUTPUT

509

protected String getBuildFailedMessage() {
return result;
}

Return the message
to print on failure

@Override
protected String getBuildSuccessfulMessage() {
return result;
}

Return the
success message

}

This logger replaces the usual BUILD SUCCESSFUL and BUILD FAILED messages at
the end of a build with whatever the evaluations of the ${ant.build.success}
and ${ant.build.failure} strings are, respectively. The parent class, the
DefaultLogger, calls getBuildSuccessfulMessage() or getBuildFailedMessage(), depending on the build’s outcome. All our logger needs to do
is evaluate the appropriate property at the end of the build. Instead of reading the
property using Project.getProperty(), the logger just creates the appropriate
${property} string and asks the project to expand it. When the property is undefined, the message will become the name of the property to set.
We can use this logger on the command line:
>ant -logger org.antbook.listeners.PersonalLogger -q \
-Dant.build.success=well-done!
well-done!
Total time: 0 seconds

Here the -logger option set the classname of the new logger, which was already on
the classpath. The -q option turned off all the output except for errors and warnings,
a feature of the DefaultLogger class that subclasses get for free.
Being able to change the message in advance is one thing, but setting it inside the
build is something else that’s potentially useful. In the build file of listing 18.8, the success and failure properties were set in the build; these are the properties that should
propagate to the output text:
ant -logger org.antbook.listeners.PersonalLogger -f test/fail.xml
Buildfile: test/fail.xml
fail:
***Installation failed, call Julio for support ***
/home/ant/listeners/test/fail.xml:9: Example build failure
Total time: 0 seconds

This build file gives us a custom error message for our end users. We do need to make
sure the new logger is selected for every build. This can be done with the ANT_OPTS
environment variable, such as here in a bash configuration file:

510

CHAPTER 18

EXTENDING ANT FURTHER

export ANT_OPTS="-logger org.antbook.listeners.PersonalLogger"

This will switch to the new logger on all command-line runs.
Avoiding trouble in custom listeners and loggers
The Ant documentation warns against loggers or listeners printing to System.out
or System.err, because doing so can create an infinite loop. The logger is handed
two PrintStream instances for output; it should use these. Listeners are not really
meant to generate output, but if they must, they should do it to some other device
such as a file. In fact, you can get away with printing to System.out and
System.err, as long as you don’t do so in messageLogged() events.
One trouble spot is the state of a project during lifecycle events. When a listener
receives a buildStarted event, the project isn’t yet fully configured. Its tasks
aren’t defined, and the default properties aren’t set up. The project isn’t ready for serious use. Similarly, when a listener has its buildFinished() method called, the
build is already finished. The listener can examine the project and its properties, but
not run any targets or tasks.
A project calls all the listeners in sequence, in the build’s current thread. Slow
operations will slow the build down. When the task is used to run
tasks in a new thread, the notifications from those tasks are raised in the new thread.
Listeners need to be thread-safe.
18.7.3

Using loggers and listeners
Historically, Ant loggers have been used for generating custom reports. The
MailLogger creates emails, and the Log4JListener and CommonsLoggingListener (which is actually a logger) generate more complex reports, including
pretty HTML logs that can be emailed around.
Nowadays, it’s the job of the continuous integration server to generate the HTML
reports and the emails, and the IDE has probably taken over from the command line
as the main way of launching Ant. In either situation, do not attempt to use your own
loggers. The IDE and continuous integration developers will have written their own
loggers, loggers that should be left alone.
If you want Ant to send out emails when a build fails, have a continuous integration tool do the work. It will catch and report problems that the loggers won’t get,
such as a missing build.xml file. There’s no need to re-implement what existing
continuous integration tools can do better.
Listeners are less troublesome, as a project can have any number of active listeners,
and listeners can be added or removed during the build. There aren’t any tasks to do
this, other than the task, which records events to a file. However, a custom task could easily create the desired listener, configure it, and
then call Project.addBuildListener() to add it to the project, or removeBuildListener() to prevent it from receiving events.

HANDLING ANT’S INPUT AND OUTPUT

511

18.7.4

Handling user input with an InputHandler
The opposite of Ant’s output system is its mechanism for handling user input.
Although Ant is designed to run without user intervention, sometimes builds use the
task to ask for input from the user. Doing so delegates the task to an
InputHandler, which is a class that handles all input from the user. The default handler reads from System.in., expecting input from a user at the console. Ant ships
with two other handlers, the PropertyFileInputHandler and the GreedyInputHandler. The first of these handlers reads input from a property file, while
the GreedyInputHandler reads the whole input stream into a single
request. Selecting this handler with the -inputhandler option lets Ant integrate
into a Unix-style pipes-and-brackets setup:
find / -name build.xml -print | ant exec -inputhandler \
org.apache.tools.ant.input.GreedyInputHandler

New classes can act as input handlers; they have to implement the org.apache
.tools.ant.input.InputHandler interface and its handleInput(InputRequest request) method.
IDE developers do all this work to stop the task from hanging under
their IDE. We aren’t covering the details of how to write a new InputHandler—only
mentioning that it is possible. If you’re writing an IDE or continuous integration tool,
then consult Ant’s documentation and source for details on how to integrate Ant’s
input handling.

18.8

EMBEDDING ANT
The final way to extend Ant is to embed it inside another Java program. This isn’t as
unusual as it sounds. Java IDEs do this to integrate Ant with their GUI. Many other
programs use it internally. It crops up in products such as Apache Tomcat, where
compiles down JSP pages into .class files. When embedded, Ant becomes
a library that can execute built-in or custom tasks. It has become a simple workflow
tool with tasks for compiling and running Java programs.
Ant can be used within any Java program. You can either create a build.xml file
and hand it off, or create a Project instance with tasks and targets via Java operations. Interestingly, there’s one way that is hard to use Ant: its static entry point,
Main.main() calls System.exit() at the end. If you want to embed this class,
you have to subclass its Main and override its exit() method. You can still use
Ant’s launcher application, specifying the new entry point via the -main argument.
However, we’ll ignore this route, as fully embedded Ant is more interesting.
Listing 18.10 shows Ant running inside another Java program. It doesn’t do
much—just runs the task. What’s important is that a Project instance has
been created and set up, with logging all wired up and running at the “info” level.

512

CHAPTER 18

EXTENDING ANT FURTHER

Listing 18.10 A private run of Ant
package org.antbook.embed;
import
import
import
import
import
import

org.apache.tools.ant.Project;
org.apache.tools.ant.BuildException;
org.apache.tools.ant.DemuxOutputStream;
org.apache.tools.ant.DefaultLogger;
org.apache.tools.ant.taskdefs.Echo;
java.io.PrintStream;

public class Embedded {
private Project project;

Create a
public Embedded() {
project
project = new Project();
project.init();
DefaultLogger logger = new DefaultLogger();
project.addBuildListener(logger);
Wire a logger
logger.setOutputPrintStream(System.out);
up to the output
streams
logger.setErrorPrintStream(System.err);
logger.setMessageOutputLevel(Project.MSG_INFO);
System.setOut(
new PrintStream(
Route output
new DemuxOutputStream(project, false)));
through the
System.setErr(
project and log
new PrintStream(
new DemuxOutputStream(project, true)));
project.fireBuildStarted();
Start the
}
build
public void run() {
Create
System.out.println("running");
a task
Echo echo=new Echo();
Bind to the
echo.setTaskName("Echo");
project
Initalize
echo.setProject(project);
the task
echo.init();
echo.setMessage("Hello, world");
Configure it
echo.execute();
Execute it
project.log("finished");
project.fireBuildFinished(null);
Stop the build
}
public static void main(String args[]) {
Embedded embed=new Embedded();
Create our class
try {
embed.run();
Run it
} catch (BuildException e) {
e.printStackTrace();
Log exceptions
}
}
}

EMBEDDING ANT

513

To configure the task, we just call the relevant set-, add-, or createmethods of the task, passing in the data we want. That’s all it takes to configure the
task. If we had wanted to expand properties or resolve paths, we would have had to
invoke the relevant Project methods before calling the task methods.
To test this program, we can run it under Ant itself, in a forked task:

failonerror="true"
fork="true">







Here is our test run of Ant inside the output of the task:
exec:
[java] running
[java]
[Echo] Hello, world
[java] finished
[java] BUILD SUCCESSFUL
[java] Total time: 0 seconds

This shows that we’ve written a new entry point to Ant. It isn’t as complex as Ant’s
own Main class, but it shows the basic techniques of running Ant and Ant tasks. You
just create a project, create and configure tasks, then bind them to the project before
you run them.
Tips on embedding Ant
• Don’t expect a very long-lived build not to leak memory. Supporting builds that
last multiple days isn’t a priority for the tool’s development.
• If you distribute a version of Ant that can be directly invoked by end users, or if
you put your version of Ant on the CLASSPATH—which has the same effect—
you take on all support responsibilities. The Ant team doesn’t support any problems related to random redistributions of Ant.
• Never use an Ant task without creating a Project and binding the task to it with
setProject(). Tasks depend on a project instance for logging and many
other operations, and they break horribly if getProject()==null.

18.9

SUMMARY
This chapter finishes our coverage of Ant with a look at the final ways to extend Ant.
Alongside Ant tasks come Ant types—types that are defined with . Many

514

CHAPTER 18

EXTENDING ANT FURTHER