Red Hat

Managing the dependencies of a seam-gen project with Ivy

Posted by    |       |    Tagged as Seam

Out of the box, seam-gen copies all the dependencies of Seam (and then some) into the lib directory of the generated project. The JAR files in that directory are then placed on the project's build path. While this approach gets you up and running quickly, it's probably not the best long-term strategy. It's difficult to determine which libraries your project actually depends on and which versions of those libraries are present. What you need is some sort of formal dependency management.

One way of managing dependencies is to use Maven 2 as the build tool, which several people have requested for seam-gen to support. However, shifting to Maven 2 is quite a large departure from the current build, which is based on Ant. In this article, I will explain how you can introduce Ivy into the Ant build to track which dependencies and versions of those dependencies your project uses and to have them fetched from a remote repository. Ivy is a sub-project of Ant and provides a collection of Ant tasks focused specifically on providing a dependency manager for Java libraries.

About this solution

What makes the setup I present in this article unique is that it separates fetching the dependencies from the build itself, so once the JARs are in place, the build can work just as it always has. That means the use of Ivy does not affect the efficiency of development that a seam-gen project provides. You also have the ability to create a distribution of the project which includes all the dependent JAR files, a key aspect of ensuring reproducibility. But there is something else that sets this solution apart.

Somewhere along the line folks seemed to have coupled shared repositories with transitive dependencies. You'll discover that it's possible to pull artifacts from the remote repositories without having to use the transitive dependency mechanism common to all Maven 2 projects (and an optional feature in Ivy).

Making way for Ivy

The first step to integrating Ivy into the project is to create a separate Ant build file, which we'll name ivy.build.xml, to host the Ivy-related tasks. Isolating these tasks in a separate file makes them more reusable and keeps the main build file clean. The Ivy build file is imported near the top of the main build file using the Ant import task:

<import file="${basedir}/ivy.build.xml"/>

In the Ivy build file, the first tasks you'll create are ones that fetch Ivy itself, in a sort of self-updating way. We'll store the Ivy JAR file in the build-lib directory at the project root to keep it isolated from the application's dependencies. To be efficient, we'll also check to see if the Ivy JAR has already been downloaded before attempting to fetch it. Here are the contents of ivy.build.xml we have talked about so far:

<?xml version="1.0"?>
<project basedir="." xmlns:ivy="antlib:org.apache.ivy.ant" name="myproject-ivy">
    <property name="ivy.install.version" value="2.0.0-beta2"/>
    <property name="ivy.jar.dir" value="${basedir}/build-lib"/>
    <property name="ivy.jar.file" value="${ivy.jar.dir}/ivy.jar"/>
    <property name="central.repo" value="http://repo1.maven.org/maven2"/>
    <property name="jboss.repo" value="http://repository.jboss.org/maven2"/>

    <target name="init-ivy">
        <available property="ivy.installed" value="true" file="${ivy.jar.file}" type="file"/>
    </target>

    <target name="download-ivy" depends="init-ivy" unless="ivy.installed">
        <mkdir dir="${ivy.jar.dir}"/>
        <echo message="Installing Ivy..."/>
        <get src="${central.repo}/org/apache/ivy/ivy/${ivy.install.version}/ivy-${ivy.install.version}.jar"
            dest="${ivy.jar.file}"/>
    </target>
<project>
NOTE I'm using Ivy 2.0.0-beta2 because there is a bug in 2.0.0-rc2 (the latest version at the time of this writing).

Before you can use Ivy, you need to have the Ivy configuration files in place. The first configuration file we will look at is the main settings file, which hosts the definitions for the artifact resolvers.

Chaining together a set of resolvers

Ivy works by mooching off of the Maven 2 repositories. However, unlike Maven, it can accommodate any hosted structure. So we need to tell Ivy where to look to find artifacts (i.e., dependencies) and what pattern it should use to locate an artifact in the remote repository as well as in the local cache. This configuration is defined in an Ivy settings file, which we will name ivy.settings.xml. We'll define three resolvers: local, central, and jboss, which we'll string together as a resolver chain and make it the default strategy.

<?xml version="1.0" encoding="UTF-8"?>
<ivysettings>
    <settings defaultResolver="default"/>
    <caches artifactPattern="[organisation]/[module]/[type]s/[artifact]-[revision](-[classifier]).[ext]" 
        checkUpToDate="true"/>
    <resolvers>
        <filesystem name="local">
            <ivy pattern="${ivy.cache.dir}/[module]/ivy-[revision].xml"/>
            <artifact pattern="${ivy.cache.dir}/[module]/[artifact]-[revision](-[classifier]).[ext]"/>
        </filesystem>
        <ibiblio name="central" m2compatible="true" usepoms="false" root="${central.repo}"
            pattern="[organisation]/[module]/[revision]/[artifact]-[revision](-[classifier]).[ext]"/>
        <ibiblio name="jboss" m2compatible="true" usepoms="false" root="${jboss.repo}"
            pattern="[organisation]/[module]/[revision]/[artifact]-[revision](-[classifier]).[ext]"/>
        <chain name="default" returnFirst="true">
            <resolver ref="local"/>
            <resolver ref="central"/>
            <resolver ref="jboss"/>
        </chain>
    </resolvers>
</ivysettings>

As I mentioned earlier, Ivy can grab JARs from almost any repository, but that also means that you have to tell it what type of repository it is dealing with. The <ibiblio> tag in the Ivy settings file applies a built-in pattern that allows Ivy to treat the repository as a standard Maven 2 repository. However, there is a bug in that pattern that makes it unable to handle non-binary artifacts, so we have to define the pattern explicitly.

You'll notice in this XML configuration two different styles of placeholder variables. The first are regular Ant property references. You can use any property you define in your Ant build prior to calling an Ivy task or any implicit property that Ivy defines (these are not well documented). As you can see here, we are using the built-in ivy.cache.dir property reference to define where the JAR files get cached locally, which happens to resolve to ${user.home}/.ivy2/cache.

The second type of placeholder variables, which are anchored by square brackets, are special Ivy replacement tokens that resolve to the segments of the artifact. The following table shows how these tokens map to Maven 2 terminology:

Ivy Maven 2
organisation group ID
module, artifact artifact ID
revision version
ext type
classifier classifier

The classifier took me a long time to discover and it requires a special setup to use in Ivy. More on that later.

Once Ivy resolves a dependency, it copies the artifact into the local cache directory, similar to how Maven 2 works. However, you have more control at the configuration level with regard to how Ivy handles updates. You also have the ability to segment the cache by project, which is done by adjusting the patterns in the <filesystem> node. This allows you to keep unrelated projects from interfering with one another. On top of this flexibility, the Ivy cache is already easier to manage than the Maven 2 local repository since it doesn't pollute the cache with its own plugins.

With this configuration in place, Ivy will search the local cache, then the central repository, and finally the JBoss repository to locate an artifact. Now the question becomes, how do you define what Ivy needs to fetch? That's where the Ivy module file comes in.

Telling Ivy about your dependencies

The ivy module file, typically named ivy.xml (though its location can be changed using the ivy.dep.file configuration property), is akin to the dependencies section of a Maven 2 POM file. It's where you define the libraries on which your project depends. Fortunately, the Ivy team does believe in XML attributes, unlike the Maven 2 radicals, so you have a lot less typing to do to define a dependency. But the most absolutely vital feature of Ivy, and what makes it infinitely more useful than Maven 2, is the fact that you can disable transitive dependencies.

I'm outspoken about my position on transitive dependencies. I see them as both evil and a silly device designed for novices (and people with way too much time on their hands). It makes your build non-reproducible and unstable and in the end causes you more work than the work you were attempting to eliminate by moving to Maven 2. This feature really is Maven's boat anchor. Trust me on this one, it's trivial to define which libraries your application depends on. There really aren't that many! And you can put all the worrying aside about exclusions. Okay, enough ranting and time to get back to the task at hand. I could go on about this topic all day!

Below is an Ivy module file for a vanilla seam-gen 2.0.3.CR1 WAR project with a couple of extras thrown in (for now I have removed the dependencies for Drools). Although Ivy has the awareness of dependency scopes (e.g., compile, runtime, test), we aren't concerned about that feature because all we are looking to do is inflate the project lib directory with the JARs we need to use the Ant build as-is.

<?xml version="1.0" encoding="UTF-8"?>
<ivy-module version="1.0">
    <info organisation="org.example" module="myproject"/>
    <configurations>
        <conf name="default" transitive="false"/>
    </configurations>
    <dependencies defaultconf="default">
        <dependency org="com.sun.facelets" name="jsf-facelets" rev="1.1.15.B1"/>
        <dependency org="commons-beanutils" name="commons-beanutils" rev="1.7.0"/>
        <dependency org="commons-digester" name="commons-digester" rev="1.7"/>
        <dependency org="javax.el" name="el-api" rev="1.0"/>
        <dependency org="javax.faces" name="jsf-api" rev="1.2_04-p02"/>
        <dependency org="javax.faces" name="jsf-impl" rev="1.2_04-p02"/>
        <dependency org="javax.persistence" name="persistence-api" rev="1.0"/>
        <dependency org="javax.servlet" name="servlet-api" rev="2.5"/>
        <dependency org="javax.transaction" name="jta" rev="1.0.1B"/>
        <dependency org="org.codehaus.groovy" name="groovy-all" rev="1.5.4"/>
        <dependency org="org.hibernate" name="hibernate-validator" rev="3.0.0.GA"/>
        <dependency org="org.jboss.el" name="jboss-el" rev="1.0_02.CR2"/>
        <dependency org="org.jboss.seam" name="jboss-seam" rev="2.0.3.CR1"/>
        <dependency org="org.jboss.seam" name="jboss-seam-debug" rev="2.0.3.CR1"/>
        <dependency org="org.jboss.seam" name="jboss-seam-ioc" rev="2.0.3.CR1"/>
        <dependency org="org.jboss.seam" name="jboss-seam-mail" rev="2.0.3.CR1"/>
        <dependency org="org.jboss.seam" name="jboss-seam-pdf" rev="2.0.3.CR1"/>
        <dependency org="org.jboss.seam" name="jboss-seam-remoting" rev="2.0.3.CR1"/>
        <dependency org="org.jboss.seam" name="jboss-seam-ui" rev="2.0.3.CR1"/>
        <dependency org="org.jbpm" name="jbpm-jpdl" rev="3.2.2"/>
        <dependency org="org.richfaces.framework" name="richfaces-api" rev="3.2.2.GA"/>
        <dependency org="org.richfaces.framework" name="richfaces-impl" rev="3.2.2.GA"/>
        <dependency org="org.richfaces.ui" name="richfaces-ui" rev="3.2.2.GA"/>
        <dependency org="org.testng" name="testng" rev="5.6"/>
    </dependencies>
</ivy-module>

You may be wondering if it's possible to eliminate the duplication in version numbers for the Seam, JSF, and RichFaces libraries (and perhaps others). I have good news for you. An Ivy module file understands Ant property references. So you can declare those versions in the ivy.build.xml file and thus have once central location to control them.

<property name="seam.version" value="2.0.3.CR1"/>
<property name="jsf.version" value="1.2_04-p02"/>
<property name="richfaces.version" value="3.2.2.GA"/>

You can now update your Ivy module file to use these property references. Here's the main Seam artifact now using an Ant property reference to manage the version:

<dependency org="org.jboss.seam" name="jboss-seam" rev="${seam.version}"/>

All we have to do now is tell Ivy to fetch the dependencies and copy them to the lib directory. For that, we switch back to our Ant build file with the Ivy-related targets.

Inflating the project

We'll create a task for inflating the lib file with the artifacts defined in the Ivy module file. This task needs to depend on a task that loads the Ivy Ant tasks and that task has to in turn depend on a task that fetches Ivy. Here are the targets:

<target name="load-ivy" depends="init-ivy,download-ivy">
    <path id="ivy.lib.path">
        <fileset dir="${ivy.jar.dir}" includes="*.jar"/>
    </path>
    <taskdef resource="org/apache/ivy/ant/antlib.xml" uri="antlib:org.apache.ivy.ant"
        classpathref="ivy.lib.path"/>
    <ivy:settings file="${basedir}/ivy.settings.xml"/>
</target>

<target name="inflate-core" depends="load-ivy">
    <ivy:retrieve pattern="${lib.dir}/[artifact].[ext]" type="jar"/>
</target>

The load-ivy target first defines the Ant tasks Ivy provides using the <taskdef> element. The Ivy tasks are available under the namespace ivy. Finally, the <ivy:settings> task bootstraps the Ivy configuration. In the inflate-core target, the <ivy:retrieve> task resolves the artifacts from the chain of repositories and copies them into the project lib directory (the lib.dir property is defined in the main Ant build file).

Again, there is a mix of Ant property references and Ivy replacement tokens to define where the JAR file is going to end up. We are mirroring the standard seam-gen setup by putting the JAR files needed for compilation into the lib directory and named without their version numbers. You can of course keep the version number by referencing the [revision] token in the pattern, but then you have to update the deployed-jars.war file to ensure that the build includes the artifacts in the deployable archive. Here's a portion of the output when the inflate-core task is run:

Buildfile: build.xml

init-ivy:

download-ivy:

load-ivy:
[ivy:settings] :: Ivy 2.0.0-beta2 - 20080225093827 :: http://ant.apache.org/ivy/ ::
[ivy:settings] :: loading settings :: file = /home/dallen/projects/myproject/ivy.settings.xml

inflate-core:
[ivy:retrieve] :: resolving dependencies :: org.example#myproject;working@sandstone
[ivy:retrieve]  confs: [default]
[ivy:retrieve]  found com.sun.facelets#jsf-facelets;1.1.15.B1 in jboss
[ivy:retrieve]  found commons-beanutils#commons-beanutils;1.7.0 in central
...
[ivy:retrieve] :: resolution report :: resolve 1622ms :: artifacts dl 121ms
        ---------------------------------------------------------------------
        |                  |            modules            ||   artifacts   |
        |       conf       | number| search|dwnlded|evicted|| number|dwnlded|
        ---------------------------------------------------------------------
        |      default     |   24  |   0   |   0   |   0   ||   24  |   0   |
        ---------------------------------------------------------------------
[ivy:retrieve] :: retrieving :: org.example#myproject
[ivy:retrieve]  confs: [default]
[ivy:retrieve]  0 artifacts copied, 24 already retrieved (0kB/34ms)

BUILD SUCCESSFUL
Total time: 3 seconds

So far, so good, but currently we are running a bit short functionality that was present before we introduced Ivy. For one, we need the JBoss Embedded JAR files and dependencies in order to be able to run tests. The project build is expecting these artifacts to be in the lib/test directory. Additionally, seam-gen includes the source artifacts for Seam in the lib/src directory, so we will want to pull down those down from the repository as well (and any other sources you want to grab). The question is, where do these artifacts fit in?

Fetching auxiliary artifacts

It's time for us to expand our dependency list to itemize the different types of dependencies. We just identified three types:

  • jar
  • source
  • test-jar

However, the <dependency> element itself has no way to make these distinctions. That is the purpose of the nested <artifact> element. This element lets us define one or more types for a dependency. In addition, we can take on a couple of additional attributes. The most vital of those attributes is the classifier.

The classifier attribute ties back into a discussion I left off with earlier. In the Maven 2 repository, sources are named by appending the suffix -sources to the end of the file name, but before the file extension. The trouble is that this introduces a new segment to the pattern. (It would have been nice if binary artifacts in the Maven 2 repository used the suffix -binary, but that's just wishful thinking).

Fortunately, Ivy understands the concept of an optional segment. (I sure hope this tip saves you time because it took me the better part of a day to track down). An optional segment is defined in an Ivy pattern by surrounding it with parenthesis. Any other text which is between the parenthesis will be ignored if the token has no value. Going back to earlier, you might recall using the following pattern in ivy.settings.xml:

[organisation]/[module]/[revision]/[artifact]-[revision](-[classifier]).[ext]

As you can see, the classifier will be appended to the URL if defined. What we need to do now is define what the classifier is for a given dependency. We'll unfold the <dependency> elements in the Ivy module file and insert one or more <artifact> elements, one of which will host the classifier attribute. Here, again, is the dependency declaration for the main Seam artifact, now paired with its sources:

<dependency org="org.jboss.seam" name="jboss-seam" rev="${seam.version}">
    <artifact name="jboss-seam" type="jar"/>
    <artifact name="jboss-seam" type="source" ext="jar" m:classifier="sources"/>
</dependency>

As you can see, the classifier attribute is defined in its own namespace. In order to prevent Ivy from complaining about the use of invalid XML syntax, you need to declare this namespace in the root element of the Ivy module file:

<ivy-module version="1.0" xmlns:m="http://ant.apache.org/ivy/maven">
    ...
</ivy-module>

When Ivy resolves the dependencies, it grabs all the artifacts, regardless of type. But what the type discriminator allows us to do is copy the artifacts to different locations according to type. We'll introduce a new target in our Ant build that copies the source artifacts to lib/src, using the type in the file name to be consist with how seam-gen lays down the project files:

<target name="inflate-source" depends="load-ivy">
    <ivy:retrieve pattern="${lib.dir}/src/[artifact]-[type]s.[ext]" type="source"/>
</target>

All that we have left to do is grab the runtime test JARs. I'll admit to you that there may be a more elegant way to accomplish this task using Ivy dependency configurations, but I couldn't make sense of that feature and the approach I am about to suggest works perfectly (sometimes we forget that something that works is better than something that is supposed to work). Here are the dependency definitions for those test JARs:

<dependency org="org.jboss.seam.embedded" name="hibernate-all" rev="${jboss-embedded.version}">
    <artifact name="hibernate-all" type="test-jar" ext="jar"/>
</dependency>
<dependency org="org.jboss.seam.embedded" name="thirdparty-all" rev="${jboss-embedded.version}">
    <artifact name="thirdparty-all" type="test-jar" ext="jar"/>
</dependency>
<dependency org="org.jboss.seam.embedded" name="jboss-embedded-all" rev="${jboss-embedded.version}">
    <artifact name="jboss-embedded-all" type="test-jar" ext="jar"/>
</dependency>

Finally, here is the Ant target that retrieves the test JARs and another Ant target to retrieve all three types in a single commmand:

<target name="inflate-test" depends="load-ivy">
    <ivy:retrieve pattern="${lib.dir}/test/[artifact].[ext]" type="test-jar"/>
</target>

<target name="inflate" depends="inflate-core,inflate-source,inflate-test"/>

Though not shown here, you might also want to create a target to purge the lib folder and another to generate a dependency report using the <ivy:report> task.

Once you have all of the Ivy configurations in place described in this article, you can wipe our your lib directory and build your project in these two steps:

ant inflate
ant explode

You are now Ivy-enabled and you're lib folder is no longer bursting at the seams.

Summary

What we set out to do is reign in the chaos of the lib folder in seam-gen projects and setup a system for managing the dependencies. Having a dependency management solution in place makes it easy to add new libraries and upgrade existing ones. The approach taken in this article was to use Ivy to define the dependencies and retrieve them from a remote repository without having to change the project build in any way (other than to import the Ivy-related build file). I purposely did not attempt to integrate Ivy into the build process because there is really no need to constantly retrieve the artifacts while developing your application. Instead, what you want is a set it and forget it approach. This strategy has the side effect of making your build reproducible and stable.

If I get around to writing another article, the next step is to eliminate the deployed-jars.list file and figure out how to instead pull this information from the Ivy module file. But that will require understanding Ivy dependency configurations better on my part.

And in case you are wondering, yes, I am considering integrating Ivy support into the seam-gen core. It just seems like a no-brainer to me. I still have hope for Maven 2, but at this point I am seeing better productivity gains from using this Ivy configuration.

Get the complete Ivy configuration and build file[1] and start using Ivy to manage your application's dependencies today!

Update: Grab the updated Ivy configuration and build file[1] to get a more concise version of the ivy.settings.xml file.

Attachments

  1. ivy-configuration.zip
back to top