Section 12.2. Extending the Task Class


12.2. Extending the Task Class

Usually, you extend an Ant task class like org.apache.tools.ant.Task when you write custom tasks. Ant comes with a selection of task classes meant to be extended:


AbstractCvsTask

Abstract CVS task class


JDBCTask

Handles JDBC configuration needed by SQL type tasks


MatchingTask

Abstract task that should be extended by tasks required to include or exclude files based on pattern matching


Pack

Abstract base class for pack tasks


Task

Generic task and the one most commonly extended


Unpack

Abstract base class for unpacking tasks

The Task class (i.e., org.apache.tools.ant.Task) is used for most of this chapter, though some samples will use MatchingTask. The methods of the Task class appear in Table 12-2.

Table 12-2. The Task class's methods

Method

Does this

void execute( )

Specifies the task should execute.

java.lang.String getDescription( )

Returns the task's description.

Location getLocation( )

Returns the file and location at which the task is supported.

Target getOwningTarget( )

Returns the target that contains this task.

RuntimeConfigurable getRuntimeConfigurableWrapper( )

Returns the wrapper class instance the task uses for runtime configuration.

java.lang.String getTaskName( )

Returns the task name (used when when logging messages from the task).

java.lang.String getTaskType( )

Returns the type of task as a string.

protected RuntimeConfigurable getWrapper( )

Returns the runtime configurable structure for this task as a RuntimeConfigurable object.

protected void handleErrorFlush(java.lang.String output)

Handles errors by logging them with ERR priority.

protected void handleErrorOutput(java.lang.String output)

Handles errors by logging them with WARN priority.

protected void handleFlush(java.lang.String output)

Handles errors by logging them with INFO priority.

protected int handleInput(byte[] buffer, int offset, int length)

Handles input requests using byte buffers.

protected void handleOutput(java.lang.String output)

Handles string output by logging it using INFO priority.

void init( )

Called automatically so the task can be initialized.

protected boolean isInvalid( )

Returns a value of TRue if this task is invalid.

void log(java.lang.String msg)

Logs a string message, giving it (default) INFO priority.

void log(java.lang.String msg, int msgLevel)

Logs a string message, giving it priority you specify.

void maybeConfigure( )

Configures the task, if it has not already been configured.

void perform( )

Performs this task. If the task is not still valid, a replacement version will be created and the task will be performed with that.

void reconfigure( )

Reconfigures a task, forcing the reconfiguration if necessary.

void setDescription(java.lang.String desc)

Specifies a string description for this task.

void setLocation(Location location)

Specifies the file and location where this task was first defined.

void setOwningTarget(Target target)

Specifies the target that contains this task.

void setRuntimeConfigurableWrapper(RuntimeConfigurable wrapper)

Sets the wrapper class that should be used for runtime configuration.

void setTaskName(java.lang.String name)

Specifes the task name (use for logging messages).

void setTaskType(java.lang.String type)

Specifies type of task in string format.


12.2.1. The Task Life Cycle

Tasks go through a well-defined life cycle, and here are the specific stages:

  1. The task is instantiated using a no-argument constructor.

  2. The task's references to its project and location inside the build file are initialized via inherited project and location variables.

  3. If the user specified an id attribute in this task, the project registers a reference to this newly created task.

  4. The task gets a reference to the target it belongs to through its inherited target variable.

  5. The init( ) method is called to initialize the task.

  6. All child elements of the task's element are created through the task's createXXX( ) methods or instantiated and added to this task with its addXXX( ) methods.

  7. All attributes of this task get set via their corresponding setXXX( ) methods.

  8. The character data sections inside the task's element are added to the task using its addText() method (if there is one).

  9. All attributes of all child elements get set using their setXXX( ) methods.

  10. The execute( ) method is called to run the task.

12.2.2. Accessing the Project and Properties in Code

When you extend the Task class, you have access to a great deal of data about the project. The Task class inherits the getProject( ) method, which returns a Project object that holds such items as the project's name and properties. You can see selected methods of the Project class in Table 12-3. You can do nearly anything with these methods, from setting a project's default target and logging text to reading property values and setting property values. That's a typical way for custom tasks to perform their work: reading property values with the Project object's getProperty( ) method and setting property values with setProperty( ). After a property has been set, it can be accessed in the build file, letting the custom task communicate with the rest of the build file.

Table 12-3. Selected Project class methods

Method

Does this

void addBuildListener(BuildListener listener)

Adds a build listener to the current project to catch build events.

void addTarget(Target target)

Adds a new target to the project at runtime.

void addTaskDefinition(java.lang.String taskName, java.lang.Class taskClass)

Adds the definition of a new task to the project.

Task createTask(java.lang.String taskType)

Creates a new task instance.

int defaultInput(byte[] buffer, int offset, int length)

Reads input data for the project from the default input stream.

void executeTarget(java.lang.String targetName)

Executes the specified target (and any targets it depends on).

void executeTargets(java.util.Vector targetNames)

Executes the specified targets in the given sequence (and the targets they depend on).

java.io.File getBaseDir( )

Returns the base directory of the project. The directory is returned as a File object.

java.util.Vector getBuildListeners( )

Returns the list of build listeners that have been added to the project.

java.io.InputStream getDefaultInputStream( )

Returns this project's default input stream as an InputStream object.

java.lang.String getDefaultTarget( )

Returns the name of the default target of the project as a string.

java.lang.String getDescription( )

Returns the project description as a string, if one has been specified.

java.lang.String getElementName(java.lang.Object element)

Returns a description of the given element as a string.

java.lang.String getName( )

Returns the name of the project if one has been specified.

java.util.Hashtable getProperties( )

Returns the project's properties table.

java.lang.String getProperty(java.lang.String name)

Returns the value of a property if it has been set in the project.

java.lang.Object getReference(java.lang.String key)

Looks up a reference in the project by ID string.

java.util.Hashtable getReferences( )

Returns a hashtable of the references in the project.

java.util.Hashtable getTargets( )

Returns the hashtable of the targets in the project.

java.util.Hashtable getTaskDefinitions( )

Returns the current task's definition hashtable.

java.util.Hashtable getUserProperties( )

Returns the user properties' hashtable.

java.lang.String getUserProperty(java.lang.String name)

Returns the value of a user property in the project if it has been set.

void init( )

Initializes the project, readying it for execution.

void log(java.lang.String message)

Writes a string message to the log. Uses the default log level, MSG_INFO.

void log(java.lang.String message, int msgLevel)

Writes a project-level message to the log. Uses message level you specify.

void log(Target target, java.lang.String message, int msgLevel)

Writes a message-level message to the log. Uses message level you specify.

void log(Task task, java.lang.String message, int msgLevel)

Writes a task-level message to the log. Uses message level you specify.

java.lang.String replaceProperties(java.lang.String value)

Replaces any occurences of ${} constructions in the given string with the value of the matching property.

java.io.File resolveFile(java.lang.String fileName)

Returns the full form of a filename.

void setBaseDir(java.io.File baseDir)

Specifies the base directory you want to use for the project.

void setBasedir(java.lang.String baseD)

Specifies the base directory, passed as a string, for the project.

void setDefault(java.lang.String defaultTarget)

Specifies the default target of the project, passed as a string.

void setDefaultInputStream(java.io.InputStream defaultInputStream)

Specifies the default System input stream as an InputStream object.

void setDescription(java.lang.String description)

Specifies the project description in string format.

void setInheritedProperty(java.lang.String name, java.lang.String value)

Specifies a user property by name and value.

void setKeepGoingMode(boolean keepGoingMode)

Specifies "keep-going" mode. In this mode, all targets that don`t depend on failed targets will be executed.

void setName(java.lang.String name)

Specifies the name of the project as a string.

void setNewProperty(java.lang.String name, java.lang.String value)

Specifies the new value of a property if no value exists.

void setProjectReference(java.lang.Object obj)

Specifies a reference to this Project using the specified object.

void setProperty(java.lang.String name, java.lang.String value)

Specifies a property, by name and value.

void setUserProperty(java.lang.String name, java.lang.String value)

Specifies a user property, by name and value.

static java.lang.String translatePath(java.lang.String toProcess)

Translates a general path into its OS-specific specific form.


Letting a custom task interact with the rest of the build through the use of properties is an important part of creating custom tasks. Take a look at Example 12-3, which is the code for an Ant task that reports the name of the project using the ant.project.name property, and the current location in the build file with the getLocation( ) method.

Example 12-3. Accessing projects and properties (ch12/projecttask/src/Project.java)
import org.apache.tools.ant.Task; public class Project extends Task  {     public void execute( )      {         String name = getProject( ).getProperty("ant.project.name");         System.out.println("Welcome to project " + name              + " at " + getLocation( ));     } }

Example 12-4 shows the build file for this custom task.

Example 12-4. Build file for accessing properties (ch12/projecttask/build.xml)
<?xml version="1.0"?> <project name="TheTask" basedir="." default="main">     <property name="src" location="src"/>     <property name="output" location="output"/>     <target name="main" depends="jar">         <taskdef name="project" classname="Project" classpath="project.jar"/>         <project/>     </target>     <target name="jar" depends="compile">         <jar destfile="project.jar" basedir="${output}"/>     </target>     <target name="compile">         <mkdir dir="${output}"/>         <javac srcdir="${src}" destdir="${output}"/>     </target> </project>

The results show that the build file reports the name of the project as set by the project element's name attribute, and the line location in the build file:

%ant Buildfile: build.xml compile:     [mkdir] Created dir: /home/steven/ant/ch12/projecttask/output     [javac] Compiling 1 source file to /home/steven/ant/ch12/projecttask/output jar:       [jar] Building jar: /home/steven/ant/ch12/projecttask/project.jar main:   [project] Welcome to project TheTask at    /home/steven/ant/ch12/projecttask/build.xml:9:  BUILD SUCCESSFUL Total time: 3 seconds

12.2.3. Handling Attributes in Custom Tasks

If your custom task supports attributes, Ant will pass the value of the attribute to your code if you have a setter method, much as you'd use in a JavaBean. For example, if you have an attribute named language, define a method, e.g., public void setLanguage(String language). Ant will pass this method the string value (after performing any needed property expansion) of the language attribute.

Strings are the most common attribute values, but you can ask Ant to perform conversions of attribute values to other data types based on the type of the argument in your setter method. Here are the possible data types and how they're handled:


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 attribute value.


Primitive types (int, short, and so forth)

Ant will convert the value of the attribute into this type and pass it to your setter method.


java.io.File

Ant will pass you a File object if the attribute value corresponds to a valid filename.


org.apache.tools.ant.types.Path

Ant will tokenize the value specified in the build file, using : and ; as path separators.


java.lang.Class

Ant will want to interpret the attribute value as a Java class name and load the named class from the system class loader.


Any other type that has a constructor with a single String argument

Ant will use this constructor to create a new instance using the name in the attribute.


A subclass of org.apache.tools.ant.types.EnumeratedAttribute

Ant will invoke this class's setValue( ) method if your task supports enumerated attributes (i.e., attributes with values that must be part of a predefined set of legal values).

What happens if more than one setter method is present for a given attribute? A method taking a String argument will not be called if more specific methods are available. If Ant could choose from other setters, only one of them will be calledbut which one is called is indeterminate, depending on your JVM.


Example 12-5 shows the code to handle a String attribute named language and displays the value assigned to this attribute in the build file. The setLanguage( ) method will be passed the attribute's value.

Example 12-5. Accessing attributes (ch12/attributetask/src/Project.java)
import org.apache.tools.ant.Task; import org.apache.tools.ant.BuildException; public class Project extends Task  {     private String language;     public void execute( ) throws BuildException      {         System.out.println("The language is " + language);     }     public void setLanguage(String language)      {         this.language = language;     } }

The build file that builds the custom task in Example 12-5 and then uses it appears in Example 12-6. In this example, Ant builds the code for the new task, project, and uses that task, setting the language attribute to "English". The code for this task reads the value of the language attribute and displays it during the build.

Example 12-6. Build file for accessing attributes (ch12/attributetask/build.xml)
<?xml version="1.0"?> <project basedir="." default="main">     <property name="src" value="src"/>     <property name="output" value="output"/>     <target name="main" depends="jar">         <taskdef name="project" classname="Project" classpath="Project.jar"/>         <project language="English"/>     </target>     <target name="compile">         <mkdir dir="${output}"/>         <javac srcdir="${src}" destdir="${output}"/>     </target>     <target name="jar" depends="compile">         <jar destfile="Project.jar" basedir="${output}"/>     </target> </project>

Here's what the build output looks like:

%ant Buildfile: build.xml compile:     [javac] Compiling 1 source file to /home/steven/ant/ch12/attributetask/output jar:       [jar] Building jar: /home/steven/ant/ch12/attributetask/Project.jar main:   [project] The language is English BUILD SUCCESSFUL Total time: 4 seconds

12.2.4. Making Builds Fail

Want to make a build fail? Make your task code throw an org.apache.tools.ant.BuildException. For example, if your custom task supports a failonerror attribute, you might use code something like this:

public void setFailonerror(boolean failOnError) {     this.fail = failOnError; } public void execute( ) throws BuildException {     if (fail) {         if error...             throw new BuildException("Attribute language is required");     } else {         ....     } }

Ant will display the text you pass to the BuildException constructor in the fail message.

12.2.5. Handling Nested Text

Ant tasks can support nested text, and custom tasks can support such text as well. Take a look at Example 12-7, which includes a project task that contains the nested text "No worries.".

Example 12-7. Build file for accessing nested text (ch12/nestedtext/build.xml)
<?xml version="1.0"?> <project basedir="." default="main">     <property name="src" value="src"/>     <property name="output" value="output"/>     <target name="main" depends="jar">         <taskdef name="project" classname="Project" classpath="Project.jar"/>         <project>No worries.</project>     </target>     <target name="compile">         <mkdir dir="${output}"/>         <javac srcdir="${src}" destdir="${output}"/>     </target>     <target name="jar" depends="compile">         <jar destfile="Project.jar" basedir="${output}"/>     </target> </project>

In your task's code, you can receive access to an element's nested text with the addText() method. The text will be passed to this method, and Example 12-8 shows how to retrieve that text and display it.

Example 12-8. Accessing nested text (ch12/nestedtext/src/Project.java)
import org.apache.tools.ant.Task; public class Project extends Task  {     String text;     public void addText(String text)      {         this.text = text;     }     public void execute( )      {         System.out.println(text);     } }

Here's what you get when you run this build file and the custom task with the nested text "No worries." in Example 12-7:

%ant Buildfile: build.xml compile:     [mkdir] Created dir: /home/steven/ant/ch12/nestedtext/output     [javac] Compiling 1 source file to /home/steven/ant/ch12/nestedtext/output jar:       [jar] Building jar: /home/steven/ant/ch12/nestedtext/Project.jar main:   [project] No worries. BUILD SUCCESSFUL Total time: 7 seconds

The supporting code for the custom task recovered the nested text and, in this case, displayed it during the build.

Want to handle properties in nested text? Use replaceProperties-(java.lang.String value), which replaces ${} style constructions in the given value with the string value of the corresponding datatypes, and returns the resulting string.


12.2.6. Handling Nested Elements

Nested text is one thing, but what if you have nested elements in a custom task? For instance, assume that your custom task has nested elements named nested, as in Example 12-9, and suppose that these elements have an attribute named language. How can you recover the values of the language attributes?

Example 12-9. Nested elements in a custom task (ch12/nestedelement/build.xml)
<?xml version="1.0"?> <project basedir="." default="main">     <property name="src" value="src"/>     <property name="output" value="output"/>     <target name="main" depends="jar">         <taskdef name="project" classname="Project" classpath="Project.jar"/>         <project>             <nested language="English"/>             <nested language="German"/>         </project>     </target>     <target name="compile">         <mkdir dir="${output}"/>         <javac srcdir="${src}" destdir="${output}"/>     </target>     <target name="jar" depends="compile">         <jar destfile="Project.jar" basedir="${output}"/>     </target> </project>

In the code supporting this custom task, shown in Example 12-10, you need a class, Nested, representing the nested element, and you can use a method named createNested( ) to handle calls from Ant for each nested element. Each time createNested( ) is called, the code adds the new nested element to a Vector named nesteds. The language attribute of each nested element is passed to the setLanguage( ) method and can be recovered with the getLanguage( ) method. After the Vector is filled, the execute() method is called and the code iterates over the Vector, displaying the language attribute value for each nested element.

Example 12-10. Handling nested elements (ch12/nestedelement/src/Project.java)
import java.util.Vector; import java.util.Iterator; import org.apache.tools.ant.Task; import org.apache.tools.ant.BuildException; public class Project extends Task  {     public void execute( )      {         for (Iterator iterator = nesteds.iterator( ); iterator.hasNext( );){             Nested element = (Nested)iterator.next( );             System.out.println("The language is " + element.getLanguage( ));         }     }     Vector nesteds = new Vector( );     public Nested createNested( )      {         Nested nested = new Nested( );         nesteds.add(nested);         return nested;     }     public class Nested     {         public Nested( ) {}         String language;         public void setLanguage(String language)          {             this.language= language;         }         public String getLanguage( )          {             return language;         }     } }

Here's what the build file looks like when running. The support code handled the nested elements and recovered the value of the language attributes:

%ant Buildfile: build.xml compile:     [javac] Compiling 1 source file to /home/steven/ant/ch12/nestedelement/output jar:       [jar] Building jar: /home/steven/ant/ch12/nestedelement/Project.jar main:   [project] The language is English   [project] The language is German BUILD SUCCESSFUL Total time: 3 seconds

12.2.7. Using Filesets

You can make your custom tasks support filesets with the right code. Example 12-11 shows a custom task, project, acting as a fileset with an include nested element. In this case, the custom project element will display all the .java files in and below the base directory.

Example 12-11. Supporting filesets in a build file (ch12/fileset/src/Project.java)
<?xml version="1.0"?> <project basedir="." default="main">     <property name="src" value="src"/>     <property name="output" value="output"/>     <target name="main" depends="jar">         <taskdef name="project" classname="Project" classpath="Project.jar"/>         <project dir="${basedir}">             <include name="**/*.java"/>         </project>     </target>     <target name="compile">         <mkdir dir="${output}"/>         <javac srcdir="${src}" destdir="${output}"/>     </target>     <target name="jar" depends="compile">         <jar destfile="Project.jar" basedir="${output}"/>     </target> </project>

To handle filesets, you extend the MatchingTask class. In this example, the code that supports the custom task reads the value assigned to the dir attribute and uses the org.apache.tools.ant.DirectoryScanner class's getIncludedFiles( ) method to scan that directory. This method returns an array of filenames, which the code displays. All the support code appears in Example 12-12.

Example 12-12. Supporting filesets (ch12/fileset/src/Project.java)
import java.io.File; import org.apache.tools.ant.Task; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.DirectoryScanner; import org.apache.tools.ant.taskdefs.MatchingTask; public class Project extends MatchingTask  {     private File directory;          public void setDir (File directory)      {         this.directory = directory;     }          public void execute( ) throws BuildException      {         DirectoryScanner directoryscanner = getDirectoryScanner(directory);         String[] files = directoryscanner.getIncludedFiles( );         for (int loopIndex = 0; loopIndex < files.length; loopIndex++) {             System.out.println(files[loopIndex]);         }     } }

Project.java is the only .java file in the project, and that's the file the custom project task picks up:

C:\ant\ch12\fileset>ant Buildfile: build.xml compile:     [javac] Compiling 1 source file to /home/steven/ant/ch12/fileset/output jar:       [jar] Building jar: /home/steven/ant/ch12/fileset/Project.jar main:   [project] src/Project.java BUILD SUCCESSFUL Total time: 4 seconds

Extending MatchingTask to support includes and excludes nested elements, you can make your task support filesets.

12.2.8. Running External Programs

Custom Ant tasks are often wrappers for existing programs. You can launch an external program from the support code for a custom task if you use the org.apache.tools.ant.taskdefs.Execute class. Example 12-13 shows how this works and launches Windows WordPad and opens the project's build file in it.

Example 12-13. Executing external programs (ch12/executetask/src/Project.java)
import java.io.IOException; import org.apache.tools.ant.Task; import org.apache.tools.ant.taskdefs.Execute; import org.apache.tools.ant.types.Commandline; public class Project extends Task  {     public void execute( )      {         Commandline commandline = new Commandline( );         commandline.setExecutable("C:\\Program Files\\Windows NT\\Accessories\\wordpad.                                        exe");         commandline.createArgument( ).setValue("C:\\ant\\ch12\\executetask\\build.xml");         Execute runner = new Execute( );         runner.setCommandline(commandline.getCommandline( ));         try {             runner.execute( );         }         catch (IOException e) {             System.out.println(e.getMessage( ));         }     } }

In this case, the code creates an org.apache.tools.ant.types.Commandline object holding the path and name of the executable to launch, uses the Commandline object's createArgument( ).setValue method to specify the file to open, and uses the execute( ) method of the org.apache.tools.ant.taskdefs.Execute class to open WordPad.

The build file for this custom task appears in Example 12-14.

Example 12-14. Build file for executing external programs (ch12/executetask/build.xml)
<?xml version="1.0"?> <project basedir="." default="main">     <property name="src" value="src"/>     <property name="output" value="output"/>     <target name="main" depends="jar">         <taskdef name="project" classname="Project" classpath="Project.jar"/>         <project/>     </target>     <target name="compile">         <mkdir dir="${output}"/>         <javac srcdir="${src}" destdir="${output}" />     </target>     <target name="jar" depends="compile">         <jar destfile="Project.jar" basedir="${output}"/>     </target> </project>

If you run this build file in Windows (after updating the hardcoded paths in the Java code as needed), it'll launch WordPad, opening build.xml.

12.2.9. Running Scripts

While discussing how to execute external programs, Ant includes an optional task named script that can run scripts such as those written in JavaScript. You need bsf.jar, from http://jakarta.apache.org/bsf/ (not the IBM version), in the Ant lib directory to run this task. You'll need one or more of these .jar files, depending on the scripting language you want to use:


jacl.jar and tcljava.jar

Resources to run TCL scripts. Get them from http://www.scriptics.com/software/java/.


jruby.jar

Resources to run Ruby scripts. Get this from http://jruby.sourceforge.net/.


js.jar

JAR file for running JavaScript code. Get it from http://www.mozilla.org/rhino/.


judo.jar

Resources to run Judoscript code. Get this from http://www.judoscript.com/index.html.


jython.jar

JAR file to run Python scripts. Get it from http://jython.sourceforge.net/.


netrexx.jar

Resources to run Rexx scripts. Get this from http://www2.hursley.ibm.com/netrexx/.


The BeanShell JAR files

You need these to run BeanShell scripts. Get them from http://www.beanshell.org/. (Ant 1.6 and later require Beanshell Version 1.3 or later.)

The attributes for the script task appear in Table 12-4.

Table 12-4. The script tasks's attributes

Attribute

Description

Required

language

Specifies the script's language. Must be a supported Apache BSF language.

Yes

src

Specifies the location of the script if it's stored in a file (as opposed to being inline).

No


In script, you can access Ant tasks with the Name.createTask method, where Name is the project's name. For instance, Example 12-15 shows how to use the echo task from JavaScript to display numbers using a loop. Ant properties are available to your script's code, as in this case, where the message property's value is displayed.

You have access to a built-in project object in scripts, so, for example, you could find the value of the message property as project.getProperty("message").


Example 12-15. Build file for executing JavaScript (ch12/script/build.xml)
<project name="js" default="main" basedir=".">     <property name="message" value="No worries."/>     <target name="main">         <script language="javascript"> <![CDATA[             echo = js.createTask("echo");             main.addTask(echo);             for (loopIndex = 1; loopIndex <= 10; loopIndex++) {                 echo.setMessage(loopIndex);                 echo.execute( );             }             echo.setMessage(message);               ]]> </script>     </target> </project>

Here's what this build file looks like at work, where JavaScript is executing the Ant echo task. Cool.

%ant Buildfile: build.xml main:      [echo] 1      [echo] 2      [echo] 3      [echo] 4      [echo] 5      [echo] 6      [echo] 7      [echo] 8      [echo] 9      [echo] 10      [echo] No worries. BUILD SUCCESSFUL Total time: 1 second

Want to work with Ant types like filesets in script? Use the project object's createDataType( ) method. Here's an example that creates Java File objects from a fileset, all in JavaScript:

importClass(java.io.File); fileset = project.createDataType("fileset"); fileset.setDir(new File(dir)); fileset.setIncludes(includes); directoryscanner = fileset.getDirectoryScanner(project); files = directoryscanner.getIncludedFiles( ); for (loopIndex=0; loopIndex < files.length; loopIndex++) {     var filename = files[loopIndex];     var file = new File(fileset.getDir(project), filename); }



    Ant. The Definitive Guide
    Ant: The Definitive Guide, 2nd Edition
    ISBN: 0596006098
    EAN: 2147483647
    Year: 2003
    Pages: 115
    Authors: Steve Holzner

    Similar book on Amazon

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net