Useful Maven Notes

Some useful things I found out about Maven and Jelly.

Remote repository sequence

If you have the following dependency in your project.xml

  <dependency>
    <groupId>axis</groupId>
    <artifactId>axis</artifactId>
    <version>1.1</version>
    <type>jar</type>
    <url>http://ws.apache.org/axis/index.html</url>
  </dependency>

Maven goes looking for the following file in the remote repositories :

  ${repo}/axis/jars/axis-1.1.jar

If you have more than one repository in the list, then the way that Maven resolves which repository to use may not be what you expect.

#
# The remote repositories Maven uses to download artifacts (jars etc) that it can't find in the local repository.
maven.repo.remote=http://capc49.ast.cam.ac.uk/maven, http://astrogrid.ast.cam.ac.uk/maven, http://www.ibiblio.org/maven

You might expect it to check the remote repositories in sequence, and use the first jar file it finds. In fact, it does check the repositories in sequence, and it does download the first one it finds.

However, it then continues with the other repositories in the sequence, checking the last modified date of the files. If it finds another file with a more recent date, it will download that one as well.

Result is that altering the sequence of remote repositories in your project.properties does not change the results you get.

#
# This list checks the cambridge repositories first.
maven.repo.remote=http://capc49.ast.cam.ac.uk/maven, http://astrogrid.ast.cam.ac.uk/maven, http://www.ibiblio.org/maven
#
# This list checks the ibiblio repositories first, but produces the same results.
maven.repo.remote=http://www.ibiblio.org/maven, http://astrogrid.ast.cam.ac.uk/maven, http://capc49.ast.cam.ac.uk/maven

The only way to prioritise one repository over another is to tweak the last modified dates of the files in the repository.

It also means that if one of the remote repositories change the file dates when they create their repository, then their copy will override the other repositories.

Finding some code examples

I was having problems trying to figure out what was and was not possible in Maven, and wanted to find some examples.

I found a good source of example code by unzipping the plugin jars bundled with maven. If you look in the plugins directory of your Maven installation, you will find a huge set of jar files, one for each of the plugins.

If you unzip these jar files (just treat them as a normal zip file), you will find each one contains a plugin.jelly and plugin.properties file.

These are a really useful source of example code and ideas on how to use Maven and Jelly.

One thing I wanted to find out was what the deploy:copy-deps task actually did. Unpack the maven-deploy-plugin-1.1.jar and you can see the Jelly code that shows you what, and how, the plugin works.

Access to individual dependencies

If you want to get at the names and versions of individual dependencies in your local repository, the following code may help.

This example just prints out the details for each of the dependencies listed in your project.xml

  <!--+
      | Iterate our list of artifacts.
      +-->
  <core:forEach var="lib" items="${pom.artifacts}">
    <ant:echo message=""/>
    <ant:echo message="Name  : ${lib.dependency.artifactId}"/>
    <ant:echo message="Group : ${lib.dependency.groupId}"/>
    <ant:echo message="Type  : ${lib.dependency.type}"/>
    <ant:echo message="Base  : ${lib.file.parent}"/>
    <ant:echo message="File  : ${lib.file.name}"/>
    <ant:echo message="Path  : ${lib.path}"/>
  </core:forEach>

If you want to do something with a specific set of jars, you can nest an if statement inside the loop.

This example copies the jars from the castor group into a separate diretory.

  <!--+
      | Create our target directory.
      +-->
  <ant:mkdir dir="castor-jars"/>
  <!--+
      | Iterate our list of artifacts.
      +-->
  <core:forEach var="lib" items="${pom.artifacts}">
    <!--+
        | Check if it is a member of the castor group.
        +-->
    <core:if test="${lib.dependency.groupId=='castor'}">
      <ant:echo message=""/>
      <ant:echo message="Found a Castor jar"/>
      <ant:echo message="Name  : ${lib.dependency.artifactId}"/>
      <ant:echo message="Group : ${lib.dependency.groupId}"/>
      <ant:echo message="Type  : ${lib.dependency.type}"/>
      <ant:echo message="Base  : ${lib.file.parent}"/>
      <ant:echo message="File  : ${lib.file.name}"/>
      <ant:echo message="Path  : ${lib.path}"/>
      <!--+
          | Copy the jar to our target directory.
          +-->
      <ant:copy todir="castor-jars" file="${lib.path}"/>
    </core:if>
  </core:forEach>

Same thing simplified without the debug messages.

  <!--+ Create our target directory +-->
  <ant:mkdir dir="castor-jars"/>
  <!--+ Iterate our list of artifacts +-->
  <core:forEach var="lib" items="${pom.artifacts}">
    <!--+ Check if it is a member of the castor group +-->
    <core:if test="${lib.dependency.groupId=='castor'}">
      <!--+ Copy the jar to our target directory +-->
      <ant:copy todir="castor-jars" file="${lib.path}"/>
    </core:if>
  </core:forEach>

Adding your own properties to your project dependencies.

You can add your own properties to your project dependencies, and then use these in your Jelly code.

For example, we can add a property called 'my-property' to some of our dependencies.

    <!--+
        | The Castor jars
        +-->
    <dependency>
        <groupId>castor</groupId>
        <artifactId>castor</artifactId>
        <version>0.9.5</version>
        <type>jar</type>
        <url>http://www.castor.org/</url>
        <properties>
            <my-property>my-value</my-property>
        </properties>
    </dependency>
    <dependency>
        <groupId>castor</groupId>
        <artifactId>castor-xml</artifactId>
        <version>0.9.5</version>
        <jar>castor-0.9.5-xml.jar</jar>
        <type>jar</type>
        <url>http://www.castor.org/</url>
        <properties>
            <my-property>my-value</my-property>
        </properties>
    </dependency>
    <!--+
        | The libraries that Castor relies on.
        +-->
    <dependency>
        <groupId>jta</groupId>
        <artifactId>jta</artifactId>
        <version>1.0.1</version>
        <type>jar</type>
        <properties>
            <my-property>my-value</my-property>
        </properties>
    </dependency>

We can then test the value of this property in our Jelly code. This example only copies jars with a property called 'my-property' set to 'my-value'.

    <ant:mkdir dir="webapp-jars"/>
    <!--+
        | Iterate our list of artifacts.
        +-->
    <core:forEach var="lib" items="${pom.artifacts}">
        <!--+
            | Check if we want to include this jar.
            +-->
        <core:if test="${lib.dependency.getProperty('my-property')=='my-value'}">
            <!--+
                | Copy the jar to our target directory.
                +-->
            <ant:copy todir="webapp-jars" file="${lib.path}"/>
        </core:if>
    </core:forEach>

Source : plugin.jelly for the maven-war-plugin, checks a property called 'war.bundle' to see if an artifact should be included in the war file.

Iterating through a list of files.

To do something to a list of files, you can use an AntFileScanner to collect a list of the files using one or more AntFileSets to specify the matching files.

You can then use the JellyForEach tag to do something to each file in the list.

    <!--+
        | Set the name of the directory we want to scan.
        +-->
    <core:set var="mydir" value="/var/projects/maven/maven-1.0-rc1/plugins"/>

    <!--+
        | Use an Ant fileScanner to create a list of matching files.
        +-->
    <ant:fileScanner var="myfiles">
        <ant:fileset dir="${mydir}" includes="*.jar"/>
    </ant:fileScanner>

    <!--+
        | Iterate our list of matching files.
        +-->
    <core:forEach var="myfile" items="${myfiles.iterator()}">
        <ant:echo message=""/>
        <ant:echo message="Base  : ${myfile.parent}"/>
        <ant:echo message="File  : ${myfile.name}"/>
        <ant:echo message="Path  : ${myfile.path}"/>
    </core:forEach>

Note that you have to use myfiles.iterator() on your AntFileScanner to get an Iterator for the list of files. Each item in the list resolves to a java.io.File object, which you can then treat as a Bean in Jelly.

Source http://jira.codehaus.org/secure/ViewIssue.jspa?key=MAVEN-528

Unpacking the plugin examples

Putting this lot together, the following target unpacks all of the plugin jar files and copies the jelly files into a separarte directory.

Result is that you get a whole directory full of interesting examples of Jelly code.

    <!--+
        | Task to unpack all the plugin jars.
        +-->
    <target name="plugin-list">
        <!--+
            | Set the name of the directory we want to unpack the jars to.
            +-->
        <core:set var="unpackdir" value="/var/projects/mumble/plugins"/>
        <!--+
            | Set the name of the directory we want to copy the jelly files to.
            +-->
        <core:set var="jellydir" value="/var/projects/mumble/jelly"/>
        <!--+
            | Create our directories.
            +-->
        <ant:mkdir dir="${unpackdir}"/>
        <ant:mkdir dir="${jellydir}"/>
        <!--+
            | Use an Ant fileScanner to create a list of plugins jars.
            +-->
        <ant:fileScanner var="pluginjars">
            <ant:fileset dir="${maven.home}/plugins" includes="*.jar"/>
        </ant:fileScanner>
        <!--+
            | Iterate our list of files.
            +-->
        <core:forEach var="pluginjar" items="${pluginjars.iterator()}">
            <ant:echo message=""/>
            <ant:echo message="Base  : ${pluginjar.parent}"/>
            <ant:echo message="File  : ${pluginjar.name}"/>
            <ant:echo message="Path  : ${pluginjar.path}"/>
            <!--+
                | Get the plugin name from the jar name.
                | Plugin jars all follow the same format maven-{name}-plugin-{version}.jar
                +-->
            <core:set var="pluginname" value="${pluginjar.name.substring(6, pluginjar.name.lastIndexOf('-plugin'))}"/>
            <ant:echo message="Name  : ${pluginname}"/>
            <!--+
                | Create a directory for the jar contents.
                +-->
            <ant:mkdir dir="${unpackdir}/${pluginname}"/>
            <!--+
                | Unpack the jar file into our target directory.
                +-->
            <ant:unzip src="${pluginjar.path}" dest="${unpackdir}/${pluginname}"/>
            <!--+
                | Copy the plugin.jelly file to our jelly directory.
                +-->
            <ant:copy vebose="true" file="${unpackdir}/${pluginname}/plugin.jelly" tofile="${jellydir}/${pluginname}.jelly"/>
        </core:forEach>
    </target>

Note the embedded Java code inside the ${} sections.

<core:set var="pluginname" value="${pluginjar.name.substring(6, pluginjar.name.lastIndexOf('-plugin'))}"/>

  • pluginjar is a java.io.File object
  • pluginjar.name calls getName(), which returns a java.lang.String
  • pluginjar.name resolves to a java.lang.String, so we can call substring() and lastIndexOf() on it

This is equivalent to

    java.io.File pluginjar ;
    java.lang.Object pluginname = pluginjar.getName().substring(6, pluginjar.getName().lastIndexOf("-plugin"))

Or, not .... Actually Jelly does not know that 'pluginjar' is a java.io.File, so it treats it as an java.lang.Object and has to use the reflection mechanism to call the getName() method.

Well ... I hope that is clear smile

How to find out what is actually in the build classpath

Ok, this took a little detectoring, but I figured out how to list all of the jars in the classpath.

Starting with the plugin.jelly file in the maven-java-plugin.

    <goal name="java:compile" ....>
        ....
        <ant:javac
            ....
            <ant:classpath>
                <ant:path refid="maven.dependency.classpath"/>
                <ant:pathelement path="${maven.build.dest}"/>
            </ant:classpath>
            ....
        </ant:javac>
        ....
    </goal>

This means that the ant:javac task uses a classpath with two elements.

  • The first element, refid="maven.dependency.classpath", is a reference to an Ant path setup elsewhere in the project.
  • The second element, path="${maven.build.dest}, just means that it includes all of the classes in the current build.

The first of these is the interesting one. This seems to be an Ant path, created by Maven, containing all of the jars to use for the build process.

We can try and check this by trying to print it out using an ant:echo statement.

    <!--+
        | Try to echo the Dependency Classpath
        +-->
    <ant:echo message="Classpath : ${maven.dependency.classpath}"/>

This does not work, because the ant:echo tag only works on properties, not on references. However, we can convert the reference into an Ant property, and then print that out.

    <!--+
        | Convert the Dependency Classpath reference into an Ant property.
        +-->
        <ant:property name="frog" refid="maven.dependency.classpath"/>
    <!--+
        | Echo the property.
        +-->
        <ant:echo message="Classpath : ${frog}"/>

This works, but gives us the whole classpath as a single string.

    [echo] Classpath : /var/projects/maven/local/repository/ant/jars/ant-1.5.4.jar:/var/projects/maven/local/repository/ant/jars/ant-optional-1.5.4.jar:/var/projects/maven/local/repository/castor/jars/castor-0.9.5.jar:/var/projects/maven/local/repository/castor/jars/castor-0.9.5-xml.jar:/var/projects/maven/local/repository/jta/jars/jta-1.0.1.jar:/var/projects/maven/local/repository/xerces/jars/xerces-2.4.0.jar:/var/projects/maven/local/repository/xml-apis/jars/xml-apis-1.0.b2.jar:/var/projects/maven/local/repository/jconfig/jars/jconfig-2.2.jar:/var/projects/maven/local/repository/hsqldb/jars/hsqldb-1.7.1.jar:/var/projects/maven/local/repository/junit/jars/junit-3.8.1.jar

It does indeed contain the list of jar files, but the format is a little difficult to use. Ideally, we want this as a list that we can manipulate ourselves.

Everything in Maven appears to hang off of the top level ProjectObjectModel , or POM. Looking the plugin.jelly examples from the various Maven plugins, a lot of them use properties or methods on the 'pom' to access the project data. So, first step is to find out what the POM actually is.

    <!--+
        | Display the class for the 'pom'.
        +-->
        <ant:echo message="POM : ${pom.getClass()}"/>

    [echo] POM : class org.apache.maven.project.Project

This tells us that, surprise surprise the 'pom' is a org.apache.maven.project.Project.

The JavaDoc for the Maven components is available here : http://maven.apache.org/apidocs/index.html

and the JavaDoc for a Maven Project is here : http://maven.apache.org/apidocs/org/apache/maven/project/Project.html

Looking at the JavaDoc there are a whole load of interesting things to play with. However, the two main ones of interest at this point are getDependencies() and getAntProject().

At first glance, getDependencies() looks like it will do what we want.

    public org.apache.commons.grant.GrantProject getAntProject()
And, in fact this does give us a list of all of the project dependencies.

We can see what is in the list using echo to display the properties and class for each item in the list.

    <!--+
        | Display the class and properties for the project dependencies.
        +-->
        <core:forEach var="item" items="${pom.getDependencies()}">
            <ant:echo message="Class : ${item.getClass()}"/>
            <ant:echo message="Item  : ${item}"/>
        </core:forEach>

    [echo] Class : class org.apache.maven.project.Dependency
    [echo] Item  : Dep[ id:ant:ant pid:ant:ant ver:1.5.4 ar:ant-1.5.4.jar jar:null ]
    [echo] Class : class org.apache.maven.project.Dependency
    [echo] Item  : Dep[ id:ant:ant-optional pid:ant:ant-optional ver:1.5.4 ar:ant-optional-1.5.4.jar jar:null ]
    [echo] Class : class org.apache.maven.project.Dependency
    [echo] Item  : Dep[ id:castor:castor pid:castor:castor ver:0.9.5 ar:castor-0.9.5.jar jar:null ]
    [echo] Class : class org.apache.maven.project.Dependency
    [echo] Item  : Dep[ id:castor:castor-xml pid:castor:castor-xml ver:0.9.5 ar:castor-0.9.5-xml.jar jar:castor-0.9.5-xml.jar ]
    ....

However, this is a list of the dependencies declared in the project.xml, not the actual list of jars in the java:compile classpath. These may not be the same thing, as will become clear later on ...

What I'm interesetd in finding is the contents of the classpath used by the java:compile goal, 'maven.dependency.classpath'.

So, back to the JavaDoc for org.apache.maven.project.Project.

The other interesting method is getAntProject(), which returns an org.apache.commons.grant.GrantProject object.

    public org.apache.commons.grant.GrantProject getAntProject()

This looks useful, because the way that java:compile goal referes to the 'maven.dependency.classpath' implies that it is a reference to Ant path object.

    <goal name="java:compile" ....>
        ....
        <ant:javac
            ....
            <ant:classpath>
                <ant:path refid="maven.dependency.classpath"/>
                <ant:pathelement path="${maven.build.dest}"/>
            </ant:classpath>
            ....
        </ant:javac>
        ....
    </goal>

Note that getAntProject() does not return an Ant Project, it returns an org.apache.commons.grant.GrantProject.

We can check this by using echo to display the class name of the object we get back from getAntProject().

    <ant:echo message="AntProject : ${pom.getAntProject().getClass()}"/>

    [echo] AntProject : class org.apache.commons.grant.GrantProject

I couldn't find out which of the commons projects this belongs to, but putting the class name into Google, I found the JavaDoc . http://jakarta.apache.org/commons/sandbox/grant/apidocs/org/apache/commons/grant/GrantProject.html

The JavaDoc describes a GrantProject as :

A subclass of an ant Project which allows installation of delegators for particular functions.

So, we do have an Ant Project, with extensions.

The way that java:compile goal uses 'maven.dependency.classpath' implies that it is a reference to Ant path object.

The JavaDoc for the Ant components isn't available on-line, but it does come packaged with the binary distributiion.

Looking at the JavaDoc for org.apache.tools.ant.Project, there isn't an obvious way to get at the paths. However, there is a method called getReference()

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

We can try seeing what we get if we ask for an object with the reference ID 'maven.dependency.classpath'.

    <ant:echo message="Reference  : ${pom.getAntProject().getReference('maven.dependency.classpath').getClass()}"/>

    [echo] Reference  : class org.apache.tools.ant.types.Path

Getting closer ... we have now resolved the reference ID 'maven.dependency.classpath' into an Ant Path object.

Looking up the JavaDoc for org.apache.tools.ant.types.Path there is a list() method that returns the path elements.

    public java.lang.String[] list()

So, what happens if we pass this array into a Jelly ForNext loop ?

    <core:forEach var="path" items="${pom.getAntProject().getReference('maven.dependency.classpath').list()}">
        <ant:echo message="Path : ${path}"/>
    </core:forEach>

    [echo] Path : /var/projects/maven/local/repository/ant/jars/ant-1.5.4.jar
    [echo] Path : /var/projects/maven/local/repository/ant/jars/ant-optional-1.5.4.jar
    [echo] Path : /var/projects/maven/local/repository/castor/jars/castor-0.9.5.jar
    [echo] Path : /var/projects/maven/local/repository/castor/jars/castor-0.9.5-xml.jar
    [echo] Path : /var/projects/maven/local/repository/jta/jars/jta-1.0.1.jar
    [echo] Path : /var/projects/maven/local/repository/xerces/jars/xerces-2.4.0.jar
    [echo] Path : /var/projects/maven/local/repository/xml-apis/jars/xml-apis-1.0.b2.jar
    [echo] Path : /var/projects/maven/local/repository/jconfig/jars/jconfig-2.2.jar
    [echo] Path : /var/projects/maven/local/repository/hsqldb/jars/hsqldb-1.7.1.jar
    [echo] Path : /var/projects/maven/local/repository/junit/jars/junit-3.8.1.jar

We now have a list of the paths in the 'maven.dependency.classpath', as used by the java:compile goal. And, at the moment, it contains each of the jars listed in the project.xml dependencies.

So, why go through all this just to print out a list of the project dependencies (again) ? Well, it turns out that the actual contents of the 'maven.dependency.classpath' may not be quite the same as the original project dependencies.

When poking around inside the plugin.jelly files for some of the plugins, I came accross the following code in the Clover plugin.

    <ant:path id="clover.classpath">
        <ant:pathelement path="${plugin.getDependencyPath('clover')}"/>
    </ant:path>

    <goal name="clover:on" ....>
        ....
        <maven:addPath id="maven.dependency.classpath" refid="clover.classpath"/>
        ....
    </goal>

The first section sets up an Ant Path, using getDependencyPath() to get the location of the Clover jar file.

The second section looks like it uses maven:addPath to add 'clover.classpath' to the 'maven.dependency.classpath'.

The JavaDoc for the maven:addPath tag is available here : http://maven.apache.org/apidocs/org/apache/maven/jelly/tags/maven/AddPathTag.html

This describes the AddPathTag as :

This class takes a given path using an id and appends another path (using refid) to it. 

So, it turns out that any of the plugins can add things to your classpath, even if they are not listed in your project.xml dependencies.

How to create a new Maven plugin

This one turned out to be not too tricky, apart from one caveat.

To create a simple plugin, first find out where Maven unpacks its plugin jars to. This defaults to a hidden directory under your home directory, ${user.home}/.maven.

If, like me, you object to yet another hidden directory in your home, then you can set this in your build.properties file.

# Maven home directory.
# Defaults to a hidden directory ${user.home}/.maven
maven.home.local=/var/projects/maven/local/home

Maven unpacks its plugin jars into a sub-directory under maven.home.local called plugins.

    {maven.home.local}
        |
        \- plugins
            |
            +- maven-antlr-plugin-1.1
            |
            +- maven-ant-plugin-1.4
            |
            +- maven-appserver-plugin-2.0-dev
            |
            + ....

To create a new plugin, just create a new directory.

    {maven.home.local}
        |
        \- plugins
            |
            +- ....
            |
            \- maven-astrogrid-plugin-1.4.1

Inside that, you need to create three files, project.xml, plugin.jelly and plugin.properties.

    {maven.home.local}
        |
        \- plugins
            |
            +- ....
            |
            \- maven-astrogrid-plugin-1.4.1
                |
                +- project.xml
                |
                +- plugin.jelly
                |
                \- plugin.properties

The project.xml is a cut-down version of the normal project.xml you would create for a top level project.

    <?xml version="1.0"?>
    <!--+
        | Maven plugin for the AstroGrid project.
        | ....
        +-->
    <project>
        <extend>../project.xml</extend>
        <pomVersion>3</pomVersion>
        <id>maven-astrogrid-plugin</id>
        <name>Maven Plug-in for AstroGrid</name>
        <currentVersion>1.4.1</currentVersion>
        <description>A Maven plugin to implement some commonly used tasks for the AstroGrid project</description>
        <shortDescription>AstroGrid project tasks</shortDescription>
        <url>http://www.astrogrid.org/</url>
        <dependencies>
            <!--+
                | The Axis webapp war file.
                +-->
            <dependency>
                <groupId>axis</groupId>
                <artifactId>axis</artifactId>
                <version>1.1</version>
                <type>war</type>
                <url>http://ws.apache.org/axis/index.html</url>
            </dependency>
            <!--+
                | The 3rd party jars that Axis depends on.
                +-->
            <dependency>
                <groupId>jaf</groupId>
                <artifactId>jaf</artifactId>
                <version>1.0.2</version>
                <type>jar</type>
            </dependency>
        </dependencies>
    </project>

The plugin.jelly is where you add your plugin goals. I'm not sure about the xml namespaces as yet, some plugins declare them, some don't.

    <?xml version="1.0" encoding="UTF-8"?>
    <!--+
        | Maven plugin for the AstroGrid project.
        | ....
        +-->
    <project
        xmlns:core="jelly:core"
        xmlns:maven="jelly:maven"
        xmlns:ant="jelly:ant"
        xmlns:util="jelly:util"
        xmlns:log="jelly:log"
        xmlns:define="jelly:define"
        xmlns:deploy="deploy"
        xmlns:astro="astro"
        >

        <!--+
            | Goal to check our plugin is active.
            +-->
        <goal name="astro:init">
            <ant:echo message=""/>
            <ant:echo message="Plugin  : ${plugin}"/>
            <ant:echo message="Version : ${plugin.currentVersion}"/>
        </goal>

That is it.

With this in place, you should be able to call your plugin goal from your main project maven.xml.

    <target name="my.test">
        <attainGoal name="astro:init"/>
    </target>

    my.test:
    astro:init:
        [echo]
        [echo] Plugin  : Maven Plug-in for AstroGrid
        [echo] Version : 1.4.1
    BUILD SUCCESSFUL

Source : http://www.devx.com/java/Article/17204/0/page/4

And now for the caveat ...

Maven appears to cache some of the information about the goals available in the current set of plugins. This information is stored in the following files, inside the plugins directory.

    {maven.home.local}
        |
        \- plugins
            |
            + ....
            |
            +- callbacks.cache
            |
            +- dynatag.cache
            |
            +- goals.cache
            |
            +- plugin-dynatag-deps.cache
            |
            \- plugins.cache

If you are working on your plugin code, and you change the name of one of your goals, e.g rename 'astro:init' to 'astro:start'. Then you will need to delete these cache files before you run Maven again.

If you don't delete these cache files, then you will spend many happy hours trying to figure out why Maven is ignoring your new goal.

How to deploy a plugin so other projects can use it

I don't know ... yet.

How to find a specific dependency from within a plugin

Initially this looks easy enough.

Poking around in the other plugin jelly files, I found that most of them use 'plugin.getDependency()' and 'plugin.getDependencyPath()'.

Our plugin lists two dependencies, the Axis war file, and the JavaActivationFramework (jaf) jar file.

    <?xml version="1.0"?>
    <!--+
        | Maven plugin for the AstroGrid project.
        | ....
        +-->
    <project>
        ....
        <dependencies>
            <!--+
                | The Axis webapp war file.
                +-->
            <dependency>
                <groupId>axis</groupId>
                <artifactId>axis</artifactId>
                <version>1.1</version>
                <type>war</type>
                <url>http://ws.apache.org/axis/index.html</url>
            </dependency>
            <!--+
                | The 3rd party jars that Axis depends on.
                +-->
            <dependency>
                <groupId>jaf</groupId>
                <artifactId>jaf</artifactId>
                <version>1.0.2</version>
                <type>jar</type>
            </dependency>
        </dependencies>
    </project>

We can get at most of the details for these using getDependency() and getDependencyPath().

    <goal name="astro:test">
        <ant:echo message=""/>
        <ant:echo message="Jaf library ...."/>
        <ant:echo message="Object   : ${plugin.getDependency('jaf')}"/>
        <ant:echo message="Class    : ${plugin.getDependency('jaf').getClass()}"/>
        <ant:echo message="Version  : ${plugin.getDependency('jaf').getVersion()}"/>
        <ant:echo message="Artifact : ${plugin.getDependency('jaf').getArtifact()}"/>
        <ant:echo message="Type     : ${plugin.getDependency('jaf').getType()}"/>
        <ant:echo message="Path     : ${plugin.getDependencyPath('jaf')}"/>

        <ant:echo message=""/>
        <ant:echo message="Axis webapp ...."/>
        <ant:echo message="Object   : ${plugin.getDependency('axis')}"/>
        <ant:echo message="Class    : ${plugin.getDependency('axis').getClass()}"/>
        <ant:echo message="Version  : ${plugin.getDependency('axis').getVersion()}"/>
        <ant:echo message="Artifact : ${plugin.getDependency('axis').getArtifact()}"/>
        <ant:echo message="Type     : ${plugin.getDependency('axis').getType()}"/>
        <ant:echo message="Path     : ${plugin.getDependencyPath('axis')}"/>
    </goal>

    [echo]
    [echo] Jaf library ....
    [echo] Object   : Dep[ id:jaf:jaf pid:jaf:jaf ver:1.0.2 ar:jaf-1.0.2.jar jar:null ]
    [echo] Class    : class org.apache.maven.project.Dependency
    [echo] Version  : 1.0.2
    [echo] Artifact : jaf-1.0.2.jar
    [echo] Type     : jar
    [echo] Path     : /var/projects/maven/local/repository/jaf/jars/jaf-1.0.2.jar
    [echo]
    [echo] Axis webapp ....
    [echo] Object   : Dep[ id:axis:axis pid:axis:axis ver:1.1 ar:axis-1.1.war jar:null ]
    [echo] Class    : class org.apache.maven.project.Dependency
    [echo] Version  : 1.1
    [echo] Artifact : axis-1.1.war
    [echo] Type     : war
    [echo] Path     :

All apart from the last one .... it does not find the path for the Axis webapp war file.

So, starting at the top, what is ${plugin}.

    <ant:echo message=""/>
    <ant:echo message="Plugin ...."/>
    <ant:echo message="Class    : ${plugin.getClass()}"/>

Turns out that this is a Project not a Plugin.

    [echo]
    [echo] Plugin ....
    [echo] Class    : class org.apache.maven.project.Project

The JavaDoc for org.apache.maven.project.Project is available here : http://maven.apache.org/apidocs/org/apache/maven/project/Project.html

There is a small clue in the JavaDoc description for getDependencyPath().

    Get an individual dependencies classpath entry.

But we are looking for a war file, which does not get put into the classpath.

The source for getDependencyPath() is available here : http://maven.apache.org/xref/org/apache/maven/project/Project.html#701

public String getDependencyPath( String depId )
    {
    return (String) dependencyPaths.get( legacyToStandardId( depId ) );
    }

And dependencyPaths is setup about here : http://maven.apache.org/xref/org/apache/maven/project/Project.html#1429

public void buildDependencyClasspath()
    throws Exception
    {
    setDependencyClasspath( DependencyClasspathBuilder.build( this ) );
    Path p = new Path( getContext().getAntProject() );
    p.setPath( getDependencyClasspath() );
    getContext().getAntProject().addReference( MavenConstants.DEPENDENCY_CLASSPATH, p );
    }

The source for DependencyClasspathBuilder is available here : http://maven.apache.org/xref/org/apache/maven/DependencyClasspathBuilder.html

And the build method is about here : http://maven.apache.org/xref/org/apache/maven/DependencyClasspathBuilder.html#85

public static String build( Project project )
    throws Exception
    {
    StringBuffer classpath = new StringBuffer();
    for ( Iterator i = project.getArtifacts().iterator(); i.hasNext(); )
        {
        Artifact artifact = (Artifact) i.next();
        Dependency d = artifact.getDependency();
        // Only add jar or ejb (MAVEN-512) dependencies to the classpath 
        if ( d.isAddedToClasspath() )
            {
            classpath.append( artifact.getPath() ).append( cps );
            project.setDependencyPath( d.getId(), artifact.getPath() );
            }
        }
    return classpath.toString();
    }

It checks if the Dependency.isAddedToClasspath() before it adds the artifact to the classpath.

The source for Dependency is available here : http://maven.apache.org/xref/org/apache/maven/project/Dependency.html

And the isAddedToClasspath() method is about here : http://maven.apache.org/xref/org/apache/maven/project/Dependency.html#357

public boolean isAddedToClasspath()
    {
    if (type != null)
        {
        String artifactType = type.getType();
        return artifactType.equals("jar") || artifactType.equalsIgnoreCase("ejb");
        }
    return false;
    }

This means that only 'jar' and 'ear' files get processed by the DependencyClasspathBuilder .

Which means that although we can use ${plugin.getDependency('axis')} to get the Dependency object for the Axis war file, we can't use getDependencyPath() to get at the path because the it has not been added to the list.

If we had the Artifact object, we could use artifact.getPath(). However, although the project has a getDependency('ident') method, there isn't the equivalent getArtifact('ident') method.

If we did have an Artifact, then we could get the corresponding Dependency using artifact.getDependency().

However, there isn't the equivalent dependency.getArtifact() method to get from a Dependency to the corresponding Artifact. Actually, there is a method called getArtifact(), but it returns a String rather than an Artifact object.

The only way I could figure out of doing this was to iterate through the list of Artifacts and grab the one we want.

    <!--+
        | Iterate through our list of plugin artifacts.
        +-->
    <core:forEach var="artifact" items="${plugin.getArtifacts()}">
        <!--+
            | Inspect each Artifact, and grab the one we want.
            +-->
        <ant:echo message="Artifact : ${artifact}"/>
        <ant:echo message="Class    : ${artifact.getClass()}"/>
        <ant:echo message="Path     : ${artifact.getPath()}"/>
        <ant:echo message="Depend   : ${artifact.getDependency()}"/>
        <ant:echo message="Type     : ${artifact.getDependency().getType()}"/>
        <ant:echo message="Ident    : ${artifact.getDependency().getId()}"/>
    </core:forEach>

For the two artifacts in our project.xml, this generates the following outpur.

    [echo]
    [echo] Artifact : org.apache.maven.repository.GenericArtifact@bb494b
    [echo] Class    : class org.apache.maven.repository.GenericArtifact
    [echo] Path     : /var/projects/maven/local/repository/jaf/jars/jaf-1.0.2.jar
    [echo] Depend   : Dep[ id:jaf:jaf pid:jaf:jaf ver:1.0.2 ar:jaf-1.0.2.jar jar:null ]
    [echo] Type     : jar
    [echo] Ident    : jaf:jaf
    [echo]
    [echo] Artifact : org.apache.maven.repository.GenericArtifact@1077fc9
    [echo] Class    : class org.apache.maven.repository.GenericArtifact
    [echo] Path     : /var/projects/maven/local/repository/axis/wars/axis-1.1.war
    [echo] Depend   : Dep[ id:axis:axis pid:axis:axis ver:1.1 ar:axis-1.1.war jar:null ]
    [echo] Type     : war
    [echo] Ident    : axis:axis

So, In theory, to catch the Axis webapp war file, we could look for an Artifact with a Dependency type of 'war' and a Dependency id of 'axis:axis'.

This seemed a good place to start :

    <!--+
        | Iterate through our list of plugin artifacts.
        +-->
    <core:forEach var="artifact" items="${plugin.getArtifacts()}">
        <!--+
            | Inspect each Artifact, and grab the one we want.
            +-->
        <core:if test="${artifact.getDependency().getType()}=='war' and ${artifact.getDependency().getId()}=='axis:axis'}">
            <ant:echo message=""/>
            <ant:echo message="Found Axis war file"/>
            <ant:echo message="Ident    : ${artifact.getDependency().getId()}"/>
            <ant:echo message="Type     : ${artifact.getDependency().getType()}"/>
            <ant:echo message="Name     : ${artifact.getName()}"/>
            <ant:echo message="Path     : ${artifact.getPath()}"/>
        </core:if>
    </core:forEach>
This didn't work, for several reasons.

Simplifying the test to check only the type, I found that Jelly does not use the equals() method to compare strings. So, the test needs to be be changed from this :

        <core:if test="${artifact.getDependency().getType()}=='war'">
to this
        <core:if test="${artifact.getDependency().getType().equals('war')}">

This works of you test for one value or the other,

        <core:if test="${artifact.getDependency().getType().equals('war')}">
or
        <core:if test="${artifact.getDependency().getId().equals('axis.axis')}">
but not both
        <core:if test="${artifact.getDependency().getType().equals('war')} and ${artifact.getDependency().getId().equals('axis.axis')}">

Next guess was that the 'and' wasn't working, so try putting both conditions together inside one set of {}. At this point I was guessing that whatever is inside the {} is Java code, so I also changed the 'and' to '&&'.

        <core:if test="${artifact.getDependency().getType().equals('war') && artifact.getDependency().getId().equals('axis:axis')}">

This goes bang because you can't have an un-escaped '&' in an XML document.

    org.xml.sax.SAXParseException: The entity name must immediately follow the '&' in the entity reference.

Try escaping the '&' with the XML entity, '&'

        <core:if test="${artifact.getDependency().getType().equals('war') &amp;&amp; artifact.getDependency().getId().equals('axis:axis')}">
This works, but is a little messy.

Turns out that you can use 'and' in the expression, which implies that the expression in the {} is not quite Java.

        <core:if test="${artifact.getDependency().getType().equals('war') and artifact.getDependency().getId().equals('axis:axis')}">

So, putting this together, we can now get the path to our Axis webapp war file.

    <!--+
        | Iterate through our list of plugin artifacts.
        +-->
    <core:forEach var="artifact" items="${plugin.getArtifacts()}">
        <!--+
            | If the artifact matches our criteria.
            +-->
        <core:if test="${artifact.getDependency().getType().equals('war') && artifact.getDependency().getId().equals('axis:axis')}">
            <ant:echo message=""/>
            <ant:echo message="Found Axis war file"/>
            <ant:echo message="Ident    : ${artifact.getDependency().getId()}"/>
            <ant:echo message="Type     : ${artifact.getDependency().getType()}"/>
            <ant:echo message="Name     : ${artifact.getName()}"/>
            <ant:echo message="Path     : ${artifact.getPath()}"/>
        </core:if>
    </core:forEach>

Unpack the Axis webapp from the Axis war file

This is where all this was leading to. What I wanted was a plugin goal that would download the Axis war file, unpack it into our build directory, and add the jars to our classpath.

Once we have the location of the Axis war file, we can use an Ant unzip task to unpack the war file into our build directory.

    <!--+
        | Unpack the Axis war file.
        +-->
    <ant:echo message=""/>
    <ant:echo message="Unpacking Axis war file"/>
    <ant:echo message="Path     : ${astro.axis.webapp.dir}"/>
    <ant:unjar src="${artifact.getPath()}" dest="${astro.axis.webapp.dir}"/>

This uses a property called 'astro.axis.webapp.dir', which is configured in the plugin.properties file.

    #
    # Location where the Axis webapp will be unpacked.
    astro.axis.webapp.dir = ${maven.build.dir}/axis/webapp
Making this a property means that it can be overriden in one of the main project property files.

Once we have unpacked the Axis webapp, we can create a classpath which includes all the jar files in the WEB-INF/lib directory.

    <!--+
        | Create an Ant classpath from the jars inside our Axis webapp.
        +-->
    <ant:path id="astro.axis.classpath">
        <pathelement location="${astro.axis.webapp.dir}/WEB-INF/lib/axis.jar"/>
        <pathelement location="${astro.axis.webapp.dir}/WEB-INF/lib/axis-ant.jar"/>
        <pathelement location="${astro.axis.webapp.dir}/WEB-INF/lib/saaj.jar"/>
        <pathelement location="${astro.axis.webapp.dir}/WEB-INF/lib/wsdl4j.jar"/>
        <pathelement location="${astro.axis.webapp.dir}/WEB-INF/lib/jaxrpc.jar"/>
        <pathelement location="${astro.axis.webapp.dir}/WEB-INF/lib/commons-logging.jar"/>
        <pathelement location="${astro.axis.webapp.dir}/WEB-INF/lib/commons-discovery.jar"/>
    </ant:path>

We can then use Project.addPath() to add this classpath to our global build classpath.

    <!--+
        | Add this to the global maven.dependency.classpath.
        +-->
    <maven:addPath id="maven.dependency.classpath" refid="astro.axis.classpath"/>

Putting all of this together, we have a plugin goal which finds the Axis war file, unpacks it, and adds the jar files into the build classpath.

    <!--+
        | Goal to prepare our Axis WebApp
        +-->
    <goal name="astro:init-axis">
        <ant:echo message=""/>
        <ant:echo message="Preparing Axis webapp ...."/>
        <!--+
            | Iterate through our list of plugin artifacts.
            +-->
        <core:forEach var="artifact" items="${plugin.getArtifacts()}">
            <!--+
                | If the artifact matches our criteria.
                +-->
            <core:if test="${artifact.getDependency().getType().equals('war') && artifact.getDependency().getId().equals('axis:axis')}">
                <ant:echo message=""/>
                <ant:echo message="Found Axis war file"/>
                <ant:echo message="Ident    : ${artifact.getDependency().getId()}"/>
                <ant:echo message="Type     : ${artifact.getDependency().getType()}"/>
                <ant:echo message="Name     : ${artifact.getName()}"/>
                <ant:echo message="Path     : ${artifact.getPath()}"/>
                <!--+
                    | Unpack the Axis war file.
                    +-->
                <ant:echo message=""/>
                <ant:echo message="Unpacking Axis war file"/>
                <ant:echo message="Path     : ${astro.axis.webapp.dir}"/>
                <ant:unjar src="${artifact.getPath()}" dest="${astro.axis.webapp.dir}"/>
                <!--+
                    | Create an Ant classpath from the jars inside our Axis webapp.
                    +-->
                <ant:path id="astro.axis.classpath">
                    <!--+
                    <ant:pathelement path="${astro.axis.webapp.dir}/WEB-INF/lib"/>
                        +-->
                    <pathelement location="${astro.axis.webapp.dir}/WEB-INF/lib/axis.jar"/>
                    <pathelement location="${astro.axis.webapp.dir}/WEB-INF/lib/axis-ant.jar"/>
                    <pathelement location="${astro.axis.webapp.dir}/WEB-INF/lib/saaj.jar"/>
                    <pathelement location="${astro.axis.webapp.dir}/WEB-INF/lib/wsdl4j.jar"/>
                    <pathelement location="${astro.axis.webapp.dir}/WEB-INF/lib/jaxrpc.jar"/>
                    <pathelement location="${astro.axis.webapp.dir}/WEB-INF/lib/commons-logging.jar"/>
                    <pathelement location="${astro.axis.webapp.dir}/WEB-INF/lib/commons-discovery.jar"/>
                </ant:path>
                <!--+
                    | Add this to the global maven.dependency.classpath.
                    +-->
                <maven:addPath id="maven.dependency.classpath" refid="astro.axis.classpath"/>
            </core:if>
        </core:forEach>
    </goal>

Now we can use the code we created earlier to print out the contents of the build path, just to check that it is doing what we want it to.

    <!--+
        | Goal to list the 'maven.dependency.classpath' classpath.
        +-->
    <goal name="astro:classpath">
        <ant:echo message=""/>
        <ant:echo message="Build classpath"/>
        <core:forEach var="path" items="${pom.getAntProject().getReference('maven.dependency.classpath').list()}">
            <ant:echo message="Path : ${path}"/>
        </core:forEach>
    </goal>

    <!--+
        | Quick check to see if it worked ...
        +-->
    <goal name="astro:check">
        <attainGoal name="astro:classpath"/>
        <attainGoal name="astro:init-axis"/>
        <attainGoal name="astro:classpath"/>
    </goal>

    astro:check:
    astro:classpath:
        [echo]
        [echo] Build classpath
        [echo] Path : /var/projects/maven/local/repository/ant/jars/ant-1.5.4.jar
        [echo] Path : /var/projects/maven/local/repository/ant/jars/ant-optional-1.5.4.jar
        [echo] Path : /var/projects/maven/local/repository/castor/jars/castor-0.9.5.jar
        [echo] Path : /var/projects/maven/local/repository/castor/jars/castor-0.9.5-xml.jar
        [echo] Path : /var/projects/maven/local/repository/jta/jars/jta-1.0.1.jar
        [echo] Path : /var/projects/maven/local/repository/xerces/jars/xerces-2.4.0.jar
        [echo] Path : /var/projects/maven/local/repository/xml-apis/jars/xml-apis-1.0.b2.jar
        [echo] Path : /var/projects/maven/local/repository/jconfig/jars/jconfig-2.2.jar
        [echo] Path : /var/projects/maven/local/repository/hsqldb/jars/hsqldb-1.7.1.jar
        [echo] Path : /var/projects/maven/local/repository/junit/jars/junit-3.8.1.jar

    astro:init-axis:
        [echo]
        [echo] Preparing Axis webapp ....
        [echo]
        [echo] Found Axis war file
        [echo] Ident    : axis:axis
        [echo] Type     : war
        [echo] Name     : axis-1.1.war
        [echo] Path     : /var/projects/maven/local/repository/axis/wars/axis-1.1.war
        [echo]
        [echo] Unpacking Axis war file
        [echo] Path     : /var/projects/astrogrid/dave-dev-20031117/community/target/axis/webapp
        [unjar] Expanding: /var/projects/maven/local/repository/axis/wars/axis-1.1.war into /var/projects/astrogrid/dave-dev-20031117/community/target/axis/webapp

    astro:classpath:
        [echo]
        [echo] Build classpath
        [echo] Path : /var/projects/maven/local/repository/ant/jars/ant-1.5.4.jar
        [echo] Path : /var/projects/maven/local/repository/ant/jars/ant-optional-1.5.4.jar
        [echo] Path : /var/projects/maven/local/repository/castor/jars/castor-0.9.5.jar
        [echo] Path : /var/projects/maven/local/repository/castor/jars/castor-0.9.5-xml.jar
        [echo] Path : /var/projects/maven/local/repository/jta/jars/jta-1.0.1.jar
        [echo] Path : /var/projects/maven/local/repository/xerces/jars/xerces-2.4.0.jar
        [echo] Path : /var/projects/maven/local/repository/xml-apis/jars/xml-apis-1.0.b2.jar
        [echo] Path : /var/projects/maven/local/repository/jconfig/jars/jconfig-2.2.jar
        [echo] Path : /var/projects/maven/local/repository/hsqldb/jars/hsqldb-1.7.1.jar
        [echo] Path : /var/projects/maven/local/repository/junit/jars/junit-3.8.1.jar
        [echo] Path : /var/projects/astrogrid/dave-dev-20031117/community/target/axis/webapp/WEB-INF/lib/axis.jar
        [echo] Path : /var/projects/astrogrid/dave-dev-20031117/community/target/axis/webapp/WEB-INF/lib/axis-ant.jar
        [echo] Path : /var/projects/astrogrid/dave-dev-20031117/community/target/axis/webapp/WEB-INF/lib/saaj.jar
        [echo] Path : /var/projects/astrogrid/dave-dev-20031117/community/target/axis/webapp/WEB-INF/lib/wsdl4j.jar
        [echo] Path : /var/projects/astrogrid/dave-dev-20031117/community/target/axis/webapp/WEB-INF/lib/jaxrpc.jar
        [echo] Path : /var/projects/astrogrid/dave-dev-20031117/community/target/axis/webapp/WEB-INF/lib/commons-logging.jar
        [echo] Path : /var/projects/astrogrid/dave-dev-20031117/community/target/axis/webapp/WEB-INF/lib/commons-discovery.jar

Why am I doing all this ?

What I set out to write was a Maven plugin that would handle all of the things we are going to need to do to publish our components as separate webapps. The reasons for doing that are discussed here : link

When I've solved all of the problems, I hope to wrap it all up as a Maven plugin that the other AstroGrid projects can use in their build process. It is better to do this in a single plugin which the other projects can use, rather than each team trying to do all this using their own separate goals and build scripts.

If the component build script supplies the jar files containing the service classes, then the plugin will supply the goals to do the following :

  • Unpack a vanilla Axis webapp to act as the basis for your component webapp
  • Generate the WebService stubs, wsdl and wsdd files from your Java interfaces
  • Install your WebService into the Axis webapp, generating the server-config.wsdd file.
  • Package the modified webapp as a war file ready for deployment.

I must admit that when I started this, I didn't expect it to be quite so messy. What started as a simple list of interesting things about Maven has turned into a huge document on hacking in Jelly (is this rude ?).

Hopefully some of this will be useful to others on the team. If only to help them avoid getting into the same kind of problems.

The initial Axis war file

I'm using a war file that I created from the webapp that comes with the Axis 1.1 distribution. At the moment, this is only available at this location link

I'll move it to one of the main AstroGrid Maven repositories later on.

Links to useful resources

-- DaveMorris - 18 Nov 2003

Topic revision: r6 - 2004-05-18 - 08:56:00 - KeithNoddle
 
AstroGrid Service Click here for the
AstroGrid Service Web
This is the AstroGrid
Development Wiki

This site is powered by the TWiki collaboration platformCopyright © by the contributing authors. All material on this collaboration platform is the property of the contributing authors.
Ideas, requests, problems regarding TWiki? Send feedback