Tuesday, April 21, 2009

Using antlib to work with Maven

If you are using ant to build your project, you may want to start looking at moving away from ant and leverage Maven instead. I've been using Maven for about a year now and after the initial pains in understanding and building a complex project with Maven, I have understood and started liking what it has to offer. However, for those still stuck in the Ant world, you can still leverage the Antlib library to leverage the dependency management capabilities of Maven. The best thing is that if you use Eclipse, you can also leverage the m2eclipse plugin for maven for projects using antlib. This helps get rid of all your dependent 3rd party jars embedded in your project.

So if you have a JEE project - typically one that builds a separate WAR and EAR file, you will probably need 3 Maven POM files.

---MyProject
|
---web
| |_
| pom.xml
|
---ear
| |_
| pom.xml
|
---pom.xml <--(toplevel pom)
|
---build.xml

All that's required in the top level pom is the following:

<project xmlns="http://maven.apache.org/POM/4.0.0" xsi="http://www.w3.org/2001/XMLSchema-instance" schemalocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelversion>4.0.0</modelversion>
<groupid>MyProject</groupid>
<artifactid>MyProject</artifactid>
<packaging>pom</packaging>
<version>1.0.0</version>
<modules>
<module>web</module>
<module>ear</module>
</modules>
</project>

The web and ear directories will have a POM file depicting the dependencies for these respective artifacts. So for example, the WAR pom file could look like:

<project 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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany.myproject</groupId>
<artifactId>myproject-war</artifactId>
<version>1.0.0</version>
<packaging>war</packaging>
<dependencies>
<!-- Application specific -->
<dependency>
<groupId>struts</groupId>
<artifactId>struts</artifactId>
<version>1.2.8</version>
</dependency>

<!-- provided -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.3</version>
<scope>provided</scope>
</dependency>
</dependencies>

</project>

Once you have these POM files setup, you can enable Maven support for the project using m2Eclipse. The "update configuration" and "update dependencies" options available through m2eclipse should create a CLASSPATH variable for your project with jars included based on those depicted in the WAR and EAR POM files.

Now for your ant scripts to work with Maven and these POM files, here's what the ant script could look like:

<?xml version="1.0" encoding="UTF-8"?>
<project basedir="." default="buildAll" name="MyProject" xmlns:artifact="urn:maven-artifact-ant">
<!-- Antlib -->
<path id="maven-ant-tasks.classpath" path="lib/maven-ant-tasks-ext-1.0.0.jar" />
<typedef resource="org/apache/maven/artifact/ant/antlib.xml"
uri="urn:maven-artifact-ant"
classpathref="maven-ant-tasks.classpath" />
<artifact:pom id="maven.war.project" file="${web.dir}/pom.xml"/>
<artifact:pom id="maven.ear.project" file="${ear.dir}/pom.xml"/>
<artifact:dependencies pathId="war.dependency.pathset">
<artifact:pom refid="maven.war.project" />
</artifact:dependencies>
<artifact:dependencies pathId="ear.dependency.pathset">
<artifact:pom refid="maven.ear.project" />
</artifact:dependencies>


<target name="compile" depends="prepare">
<condition property="debug.flag" value="on">
<istrue value="${localbuild}" />
</condition>
<condition property="debug.flag" value="off">
<isfalse value="${localbuild}" />
</condition>
<echo>Compiling source files, debug turned on</echo>

<!-- Load dependency set -->
<javac debug="on" deprecation="${deprecation.flag}" destdir="${classes.dir}" optimize="${optimize.flag}" source="1.6" target="1.6">
<src path="${gensrc.dir}" />
<src path="${src.dir}" />
<exclude name="${cvs.dirs}" />
<classpath refid="war.dependency.pathset" />
<classpath refid="ear.dependency.pathset" />
</javac>
</target>

<target name="buildWebModule" description="Create the Project Web WAR" depends="compile">
<!-- Copy dependencies to temporary web lib -->
<artifact:dependencies filesetId="war.dependency.fileset" useScope="runtime">
<artifact:pom refid="maven.war.project" />
</artifact:dependencies>
<delete dir="${web.build.dir}/lib"/>
<copy todir="${web.build.dir}/lib" flatten="true">
<fileset refid="war.dependency.fileset" />
</copy>

<!-- Create WAR -->
<war destfile="${war.path}" webxml="${web-inf.dir}/web.xml" manifest="${web.dir}/META-INF/MANIFEST.MF">
<fileset dir="${web.dir}">
<include name="**" />
</fileset>
<classes dir="${classes.dir}"/>
<lib dir="${web.build.dir}/lib"/>
<webinf dir="${web.build.dir}"/>
</war>
</target>
</project>

And you can expand the build.xml for creating the EAR file similarly. Some caveats here: notice the usescope for the WAR dependencies (useScope="runtime"). This is critical to get dependencies that are only needed to be included in your WAR versus ones which are already in the App server classpath. However, the problem here is that Antlib doesn't work exactly like to the tune of the configuration specified for a jar in the Maven POM. I had to actually make a code change in the antlib source for it to behave exactly as Maven does for dependencies marked with a scope of provided as well as the optional flag in the POM file. The change is a simple one and is listed below if you want to have this behavior. Check out the source code using the information here.

The code needs to be added to the DependenciesTask.java file:
 (line 198)
for ( Iterator i = result.getArtifacts().iterator(); i.hasNext(); )
{
Artifact artifact = (Artifact) i.next();

// Added condition to recognize optional flag and filter artifacts based on useScope
if (!(artifact.isOptional() && (artifact.getScope()
.equals(Artifact.SCOPE_RUNTIME) || (useScope != null && useScope
.equals(Artifact.SCOPE_RUNTIME)))))
addArtifactToResult(localRepo, artifact, fileSet, fileList);

versions.add( artifact.getVersion() );

The changes I've listed above should help you migrate your application over to Maven easily when you or your organization decides to adopt Maven completely.