Using the Maven Ant Tasks from within an Ant based Maven Plugin

Are you developing an Apache Ant based Maven Plugin? Do you have problems to use the Maven Ant Tasks from within your implementation? In the following article we describe what steps are necessary to use the Maven Ant Tasks within an Ant based Maven plugin.

The Scenario

We develop JEE applications for our customers which usually target the JBoss application server. In one project we developed an Ant script which was used to setup a jboss installation from scratch. This script turned out to be really useful as we used it to setup a new developer machine in seconds. We also used it within our cargo integration tests to prepare the server installation.

This script uses the Ant tasks for Maven to fetch some additional libraries from a maven repository. These libraries are used, to customize the server installation.

The Maven Ant Tasks enables several of Maven’s artifact handling features to be used from within an Ant build.
http://maven.apache.org/ant-tasks/index.html

As we tend to use Maven for all of our new projects we planned to provide the same functionality as Maven plugin. The possibility to implement Maven Plugins using Ant seemed to be a promising alternative. Using this approach it should be possible to simply wrap the existing script with a Maven Plugin. Unfortunately we had some issues using the Maven Ant tasks within the Ant based mojo.

See http://maven.apache.org/guides/plugin/guide-ant-plugin-development.html for a general guide how to develop Ant based Maven Plugins. In the Maven terminology the implementation of a plugin is called a mojo (http://maven.apache.org/guides/introduction/introduction-to-plugins.html).

Lost in Maven

The usual way to add custom tasks to Ant is to use a typedef inside the script which is configured with the path to the .jar file containing the extra Ant tasks. This did not seem like a good approach for our Ant based mojo as the plugin should run on any machine and thus the .jar is not available locally. Also it did not seem to be a good approach to add the .jar as resource to our mojo. Instead we wanted to leverage the maven dependency resolution. It was natural to define a dependency on the maven-ant-tasks in our mojo pom dependencies:

<dependencies>
   <dependency>
      <groupId>org.apache.maven</groupId>
      <artifactId>maven-script-ant</artifactId>
      <version>2.1.0</version>
   </dependency>

   <dependency>
      <groupId>org.apache.maven</groupId>
      <artifactId>maven-ant-tasks</artifactId>
      <version>2.1.0</version>
   </dependency>
</dependencies>

The sourcecode of our ant based Maven Plugin.

<project name="install" basedir="." xmlns:artifact="urn:maven-artifact-ant"
         xmlns:integration="urn:maven-ant-integration">

    <target name="init">
        <typedef resource="org/apache/maven/artifact/ant/antlib.xml"
           uri="antlib:org.apache.maven.artifact.ant" />
    </target>

    <target name="hello" depends="init" >
       <artifact:dependencies pathId="dependency.classpath">
          <dependency groupId="log4j" 
                  artifactId="log4j" 
                  version="1.2.14" />
       </artifact:dependencies>

       <property name="classpath" refid="dependency.classpath" />

       <echo message="Classpath is ${classpath}" />
    </target>
</project>

See the documentation of the ant typedef task for more details how to initialize a custom task from within ant.

Unfortunately this did not work. While it was possible to install the mojo it was not possible to run it due to some errors popping out of the Maven internals:


------------------------------------------------------------------------
[...].AntMojoComponentFactory cannot be cast to [...].ComponentFactory
------------------------------------------------------------------------
Trace
java.lang.ClassCastException: [...].AntMojoComponentFactory cannot be cast to [...].ComponentFactory
at [...].DefaultComponentFactoryManager.findComponentFactory(DefaultComponentFactoryManager.java:68)
at [...].DefaultPlexusContainer.createComponentInstance(DefaultPlexusContainer.java:1457)
at [...].AbstractComponentManager.createComponentInstance(AbstractComponentManager.java:93)
...

Searching Google it turned out that others already had met the same issues http://www.mail-archive.com/dev@maven.apache.org/msg70989.html. However they had not find a solution or at least did not publish it.

In general there seems to be no mechanism to access the Maven classpath’s from inside an Ant based mojo. But that’s another story.

Our Workaround

After some digging we found the following solution. The first step is to remove the Maven Ant Tasks dependency from the pom as this dependency interferes with the Maven runtime:

<project>
	<packaging>maven-plugin</packaging>

	<dependencies>
		<dependency>
			<groupId>org.apache.maven</groupId>
			<artifactId>maven-script-ant</artifactId>
			<version>2.1.0</version>
		</dependency>

		<dependency>
                        <!-- is required if you want to use optional ant tasks -->
			<groupId>org.apache.ant</groupId>
			<artifactId>ant-nodeps</artifactId>
			<version>1.7.1</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<artifactId>maven-plugin-plugin</artifactId>
				<version>2.5</version>

				<!-- Add the Ant plugin tools -->
				<dependencies>
					<dependency>
						<groupId>org.apache.maven.plugin-tools</groupId>
						<artifactId>maven-plugin-tools-ant</artifactId>
						<version>2.5.1</version>
					</dependency>
				</dependencies>
			</plugin>
		</plugins>
	</build>
</project>

Next we tried to use the dependency resolution from within the Ant script. The resolution is provided as a service by the Maven runtime. To be able to use this service you need to declare a dependency on that service.
Within the Ant mojo descriptor we declared to the necessary Maven components.

The Ant Mojo descriptor tells Maven which Maven goals are provided by the script, which parameters it supports and what Maven components are required by it.

Plugin Metadata: src/main/scripts/antmojo.mojos.xml

<pluginMetadata xmlns="http://maven.apache.org/POM/4.0.0" 
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/plugin-metadata-1.0.0.xsd">
	
<mojos>
	<mojo>
                <goal>hello</goal>
        
                <!-- this element refers to the Ant target we'll invoke -->
                <call>hello</call>
           
                <description>
                    Say Hello, World.
                </description>

      		<components>
			<component>
				<role>org.apache.maven.artifact.factory.ArtifactFactory
				</role>
			</component>

			<component>
				<role>org.apache.maven.artifact.resolver.ArtifactResolver
				</role>
			</component>
		</components>

		<parameters>
			<parameter>
				<name>artifactFactory</name>
				<property>artifactFactory</property>
				<required>true</required>
				<readonly>true</readonly>

				<expression>
				   ${component.org.apache.maven.artifact.factory.ArtifactFactory}
				</expression>
				<type>org.apache.maven.artifact.factory.ArtifactFactory</type>
				<description>maven artifact factory</description>
			</parameter>

			<parameter>
				<name>artifactResolver</name>
				<property>artifactResolver</property>
				<required>true</required>
				<readonly>true</readonly>
				<expression>
				   ${component.org.apache.maven.artifact.resolver.ArtifactResolver}
				</expression>
				<type>org.apache.maven.artifact.resolver.ArtifactResolver</type>
				<description>maven artifact resolver</description>
			</parameter>

			<parameter>
				<name>localRepository</name>
				<property>localRepository</property>
				<required>true</required>
				<readonly>true</readonly>

				<expression>
					${localRepository}
				</expression>
				<type>org.apache.maven.artifact.repository.ArtifactRepository</type>
				<description>maven artifact resolver</description>
			</parameter>
				
			<parameter>
				<name>remoteRepositories</name>
				<property>remoteRepositories</property>
				<required>true</required>
				<readonly>true</readonly>
				
				<expression>${project.remoteArtifactRepositories}</expression>
				<type>java.util.List</type>
				<description>repositories</description>
			</parameter>
		</parameters>

	</mojo>
</mojos>
</pluginMetadata>

Parameters with a primitive type directly can be used within your Ant script. They are available as Ant properties. However it is not that simple to use the Maven components required to use the dependency resolution. These are types Ant does not know how to handle. An alternative we used was to add a custom Ant task to the project. This task uses the Maven components to lookup the Maven Ant Tasks artifacts and makes it available to the Ant script as an Ant Path reference.

public class InitAntTask extends Task {
	public void execute() throws BuildException {
                // lookup Maven components
		ArtifactFactory artifactFactory = (ArtifactFactory) getProject()
				.getReference("org.apache.maven.artifact.factory.ArtifactFactory_null");
		
		ArtifactResolver artifactResolver = (ArtifactResolver) getProject()
                                .getReference("org.apache.maven.artifact.resolver.ArtifactResolver_null");
		
                // resolve Maven Ant Tasks artifact
		ArtifactRepository artifactRepository = (ArtifactRepository) getProject().getReference("localRepository");
		
		Artifact artifact = artifactFactory.createArtifact("org.apache.maven", "maven-ant-tasks", "2.1.0", "runtime", "jar");
		
		List repositories = (List) getProject().getReference("remoteRepositories");
		
		try {
			artifactResolver.resolve(artifact, repositories, artifactRepository);
			
			Path path = new Path(getProject());
			path.setLocation(artifact.getFile());
			
                        // add a path reference to the ant project
			getProject().addReference("maven-ant-tasks.classpath", path);
		} catch (ArtifactResolutionException e) {
			throw new BuildException();
		} catch (ArtifactNotFoundException e) {
			throw new BuildException();
		}
	}
}

You can find more details on how to use the Maven API here.

This custom Ant tasks adds the path reference maven-ant-tasks.classpath to the current Ant project. To be executed we need to add the custom task to our Ant mojo and to execute it:

File: src/main/scripts/antmojo.build.xml

<project name="install" basedir="." xmlns:artifact="urn:maven-artifact-ant" xmlns:integration="urn:maven-ant-integration">
	<target name="init:maven">
                <!-- add our custom task -->
		<typedef resource="antlib.xml" uri="urn:maven-ant-integration" />

                <!-- execute our custom task -->
		<integration:initialize />

                <!-- use the new path reference -->
		<typedef resource="org/apache/maven/artifact/ant/antlib.xml" uri="urn:maven-artifact-ant">
			<classpath refid="maven-ant-tasks.classpath" />
		</typedef>
	</target>
</project>

The missing bit is the antlib definition of our custom ant task:

File: src/main/resources/antlib.xml

<antlib>
  <!-- Tasks -->
  <taskdef name="initialize" classname="InitAntTask"/>
</antlib>

See the documentation of the ant antlib concept.

Combining everything we get the following directory layout:


/
|
+- pom.xml
|
+- /src
| |
| +- /main
| | |
| | +- /scripts (source for script-driven plugins)
| | | |
| | | +- mytarget.mojos.xml
| | | +- mytarget.build.xml
| | |
| | +- /java
| | | |
| | | +- InitAntTask.java
| | |
| | +- /resources
| | | |
| | | +- antlib.xml
...

With this approach we were able to finish our Ant based mojo. Using mvn groupId:artifactId:version:antmojo it’s possible to run it from the commandline. It’s also possible to bind it to a maven lifecycle. We do this within integration test projects which use cargo to prepare the server installation.

Maven 3

With the upcoming Maven 3 it’s interesting to see if our workaround is still working. The Maven developers promised Maven 3 to be fully backwards compatible so we tried out the currently available Maven 3.0-alpha7. This did not work. Initially we ran into a minor issue. The lookup names of the Maven components ArtifactFactory and ArtifactResolver changed. Using Maven 2.2.1 we had to use org.apache.maven.artifact.factory.ArtifactFactory_null. Using Maven 3 this changed to org.apache.maven.artifact.factory.ArtifactFactory. More serious was a NoSuchMethodError exception. It seems that this is caused by some of the Maven API changes (This is a known issue: http://jira.codehaus.org/browse/MANTTASKS-165). As a workaround we added the attribute reverseloader=”true” to the typedef task within the Ant mojo. The downside is that this attribute is deprecated in Ant 1.7.1 and seems to be removed in Ant 1.8. Hopefully the Jira issue gets fixed until Maven 3 is finally released. Finally we needed to set this attribute depending on the Maven version currently running.

Changes to InitAntTask

public void execute() {
       final ArtifactFactory artifactFactory = lookupComponent("org.apache.maven.artifact.factory.ArtifactFactory");

       final ArtifactResolver artifactResolver = lookupComponent("org.apache.maven.artifact.resolver.ArtifactResolver");

       getProject().setProperty("maven.version.major", Integer.toString(getMavenVersion()));
      
 ...
}

private <T> T lookupComponent(String name) {
        T result = (T) getProject().getReference(name + "_null");
        if (result == null) {
            // Maven 3.0-alpha7
            result = (T) getProject().getReference(name + "_");
        }

        if (result == null) {
            throw new BuildException("could not lookup Maven component " + name);
        }

        return result;
}

private int getMavenVersion() throws ComponentLookupException {
        PlexusContainer container = ((MavenSession)getProject().getReference("session")).getContainer();

        RuntimeInformation rti = (RuntimeInformation) container.lookup(RuntimeInformation.class.getName());

        ArtifactVersion version = rti.getApplicationVersion();

        return version.getMajorVersion();
}

Changes to our ant mojo

<target name="init:maven">
      ...
      <condition property="isMaven3" value="true" else="false">
        <equals arg1="3" arg2="${maven.version.major}" />
      </condition>

      <typedef resource="org/apache/maven/artifact/ant/antlib.xml" uri="urn:maven-artifact-ant" reverseloader="${isMaven3}">
          <classpath refid="maven-ant-tasks.classpath"/>
      </typedef>
</target>

Summary

In this article we showed how to use the Maven tasks for ant within an Ant based mojo. The currently available combination of the Ant tasks and Maven does not solve this easily. We used a custom Ant tasks which prepares an Ant path reference containing the Maven tasks for Ant. This path reference is then used to initialize the Ant tasks within the Ant script.

6 thoughts on “Using the Maven Ant Tasks from within an Ant based Maven Plugin

  1. Thank you for your article. It was very helpful. We are going to migrate our legacy java based m2 plugins to ant.
    The only typo I found is “mytarget.mojo.xml” should be replaced by “mytarget.mojos.xml”

  2. Thanks for the article. I was stuck at a classloader issue on the ant mojos that use maven ant tasks and this was very helpful. Have you notices any performance degrade because of the reverseloader attribute.

  3. Hi,

    first of all: great article!

    I like the idea of using mavens dependency resolution, instead of having all jars in some fixed location.

    So I followed you blog, but end up at the custom ant task. During plugin execution, the ant task can not resolve the ArtifactFactory and ArtifactResolver objects.

    I’ve already debugged into the plugin and checked the reference map, which did not contain any of these two objects.

    I’m using the most recent versions of the referenced plugins.

    Any idea whats wrong?

    Thanks in advance
    Andreas

  4. Iterating over all items in reference map via getProject().gerReferences() only shows the following keys:
    [integration:initialize] dcbuild.lib.path
    [integration:initialize] ant.targets
    [integration:initialize] remoteRepositories
    [integration:initialize] ant.PropertyHelper
    [integration:initialize] ant.parsing.context
    [integration:initialize] maven.compile.classpath
    [integration:initialize] localRepository
    [integration:initialize] ant.ComponentHelper
    [integration:initialize] session
    [integration:initialize] maven.plugin.classpath
    [integration:initialize] maven.test.classpath
    [integration:initialize] project
    [integration:initialize] mojoExecution
    [integration:initialize] ant.projectHelper
    [integration:initialize] maven.runtime.classpath
    [integration:initialize] maven.dependency.classpath

    no ArtifactResolver or ArtifactFactory objects 😦

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s