ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Maven: Trove of Tips Maven: Trove of Tips

by Andreas Schaefer
08/04/2004

When I looked at Maven the first time, I thought it was an overblown and complex tool that would never replace Ant. So far, Ant was doing its job quite well and I thought, "Why should I bother to delve into an unknown tool when there's no immediate need?" How little did I know.

Nevertheless, projects like AspectWerkz, XDoclet and others were using Maven, and I was still wondering if they would know something I did not. So when I started my JDoppio project and started writing my Ant build script, I eventually got this deja vu feeling of doing the same thing over and over again. Figuring there was nothing to lose, I decided to use Maven as a test. After downloading Maven and checking out its web site, I started to write my project object descriptor, project.xml, and after 15 minutes I could compile and test my project. Since then, I've used Maven to build my project and never looked back. So far, I can realize any requirements that the project might require, no matter how advanced they are -- some of which I could have hardly done with Ant.

Many developers responded to my success by saying that they would like to use Maven, but do not like to change from a well-known tool and enter uncharted territory. I have to admit that using Maven needs some mind bending, and handing over to Maven some control that was previously held by the developer, but in my honest opinion, it is worthwhile. Still, I recommend playing around with Maven before migrating a project, because you need some understanding of how Maven works and how to go along with Maven to get the most out of it.

This article will show some of the tips and tricks I figured out with JDoppio. Hopefully, I can give you a hint how to proceed and where to get help. In the end, Maven is all about helping you, fellow developers and users, to save time and money building projects, project web sites, and distributions. But this requires that you know how to use Maven in a way that fits your project best.

Project Object Model: project.xml

The project object model, or POM, is the heart of the Maven build tool, even though it is the smallest part of the tool. Here I do not want to give a full description of the POM; instead, I want to focus on what will be important later on. The POM file begins with general project information, which is mostly used to generate the project web site. After that, the POM allows you to specify different versions of a project based on CVS tags. This allows you to create release branches and keep them separate from the main development. After listing the developers and the distribution license, the POM defines the dependencies of the project on other archives and tools. This section looks like this:

<dependencies>
   <dependency>
      <groupId>ant</groupId>
      <artifactId>ant</artifactId>
      <version>1.6.1</version>
      <properties>
         <server.lib>true</server.lib>
      </properties>
   </dependency>
   ...
</dependencies>

Maven creates a local repository under ~/.maven/repository that is used to store archives from a remote site, as well as archives generated and installed locally. Its layout looks like this:

repository
+-- <group id>
  +-- <type>s
    +-- <artifact id>-<version>.<type>
...

In the POM outlined above it would try to find the archive ~/.maven/repository/ant/jars/ant-1.6.1.jar, because the type is set to .jar by default. If this archive could not be found locally, it would try in the current remote repository with a relative URL of /ant/jars/ant-1.6.1.jar. You can also browse the remote repository if you need to figure out the group and artifact IDs and the support version of an archive. Note that a group can contain more than one archive with different names and versions. In addition, you can add properties to a particular archive in the dependency list which can be used later to identify archives you need for a particular purpose, and we will use this later. In the example above, the Ant 1.6.1 dependency has a property named server.lib with the value true.

After that, the build element describes what is built and how it is tested:

<build>
   <sourceDirectory>
      src/java
   </sourceDirectory>
   <unitTestSourceDirectory>
      test/src/java
   </unitTestSourceDirectory>

   <unitTest>
      <includes>
         <include>**/*Test.java</include>
      </includes>
   </unitTest>

   <resources>
      <resource>
         <directory>
   ${basedir}/src/resource/logging</directory>
         <includes>
            <include>log4j.xml</include>
         </includes>
      </resource>
   </resources>
</build>

First, this specifies the source directory -- Maven will try to compile all of the classes in this directory and its subdirectories. Then the unit test source directory specifies where the jUnit tests can be found. The unit test element then lets you specify which classes are test cases and should be executed when Maven tests the project. Finally, you can specify resources needed to build distributions, etc. Note: a complete documentation of the POM can be found on the Maven site.

If everything is set up, the source and jUnit test cases are in place, then you can compile and test your project by just entering:

maven jar:jar

on the command line. If you are offline, you can try to run Maven with the -o option so that it will not try to download archives from the remove repository; however, in this case, a build could fail for want of a needed archive. Typical Maven output appears as follows:

$ maven
 __  __
|  \/  |__ _Apache__ ___
| |\/| / _` \ V / -_) ' \  ~ intelligent projects ~
|_|  |_\__,_|\_/\___|_||_|  v. 1.0-rc3

BUILD SUCCESSFUL
Total time: 1 seconds
Finished at: Tue Jun 22 13:44:36 PDT 2004

Having finished the first step of setting up a Maven project, we are ready to look at some of the advanced features of Maven.

Goals are What Drives Maven

When Maven is executed, it will start with the given goal. First, it will execute what is specified to be run before the goal is executed, then the goal itself, and finally, what is specified to be executed after the goal. Maven comes with a ton of goals ready to be used; these can be viewed with maven -g, which produces output that looks like this:

$ maven -g
 __  __
|  \/  |__ _Apache__ ___
| |\/| / _` \ V / -_) ' \  ~ intelligent projects
|_|  |_\__,_|\_/\___|_||_|  v. 1.0-rc3

Available [Plugins] / Goals
===========================
  genapp  .........................
   Generate Application based on a template
  html2xdoc  ...................... 
   Generates XDoc documentation from normal
                                    HTML files
  jdiff  ..........................
   generate an api difference report between
                                    versions
  junitdoclet  ....................
   Generate unit tests

[announcement]
   Generate release announcement
  generate  .......................
   Generate release announcement
...
[java]
 ( NO DEFAULT GOAL )
  compile  ........................
   Compile the project
  jar  ............................
   Create the deliverable jar file.
  jar-resources  ..................
   Copy any resources that must be present in
   the deployed JAR file
  prepare-filesystem  .............
   Create the directory structure needed to

This listing indicates that in order to compile the project, you have to execute Maven with maven java:compile, and to create a .jar file, you execute maven java:jar. (Note: this goal is deprecated and you should now use jar:jar instead.) Of course, most of these goals also have other goals to be execute before or after the goal itself. For example, the goal java:compile will have to execute the goal java:prepare-filesystem beforehand to make sure the necessary directory structure is in place.

You may wonder where these goals and their dependencies are coming from. You can easily figure it out by looking in the local plugin directory of Maven, in the ~/.maven/cache directory. There you will find a directory such as maven-java-plugin-1.4 containing a POM as well as the project.jelly file:

<project 
  xmlns:j="jelly:core"
  xmlns:ant="jelly:ant"
  xmlns:define="jelly:define"
  xmlns:maven="jelly:maven">

  <goal name="java:prepare-filesystem"
        description="Create the directory
           structure needed to compile">
<ant:mkdir dir="${maven.build.dest}"/>
  </goal>

  <goal name="java:compile"
        description="Compile the project"
        prereqs="java:prepare-filesystem">
    <ant:echo>
       Compiling to ${maven.build.dest}
    </ant:echo>
    <j:choose>
<j:when test="${sourcesPresent == 'true'}">
        <ant:javac
...

This file is a Jelly Script file, with the project element specifying all additional Jelly tag libraries it includes, along with the namespace of these libraries. Even though you could omit the constant specification of the namespace -- <mkdir ...> instead of <ant:mkdir ...> -- I think it is good practice to include namespaces, for documentation as well as the clarity of the script.

Further on in the code, we find the goal java:compile, with the prerequisite java:prepare-filesystem. The body of this goal specifies how the project is compiled. In this body, Ant and Jelly tags are used to compile the Java code. As you probably guessed, Jelly tags check to see if Java source is present and otherwise omits the compilation. This scripting ability gives Maven a big advantage over Ant, even though Maven is meant not to compete with Ant, but rather utilize it when possible.

But what do you do when there is no goal available to do what you need? In this case, you can write your own extension, which will be called maven.xml and located in the same directory as the POM. This file looks the same as the one above and more or less does the same thing, except that it is private to the project. In this file, you can do one of the following:

Warning: you may be tempted to create a ton of scripts to do whatever you need. I recommend searching the Maven plugins first to see if you can find a plugin to do what you need. Not only will this make your file smaller and easier to maintain, but the plugin will also be updated over time, freeing you to focus on your own development. In addition, looking at these plugins can teach you a lot about using Maven.

A simple Maven extension looks like this:

`
<project default="jdoppio:build"
         xmlns:j="jelly:core"
         xmlns:maven="jelly:maven"
         xmlns:deploy="deploy"
>

   <goal name="jdoppio:build"
         description="Build all JDoppio
            components">
      <maven:reactor basedir="${basedir}"
             includes="**/project.xml"
             goals="jar:install"
             banner="Installing:"
             ignoreFailures="false" />
   </goal>
...

This script defines the project's own build goal: jdoppio:build, better expressing what it does than a simple java:compile. Inside, it is using Maven's reactor to execute several sub-projects using the jar:install goal of each, which compiles and tests the projects. jdoppio:build then builds a distribution and copies into the local repository. The last step is important because other sub-projects depend on it, and will use the POM's dependencies to use these archives from the local repository.

Maven Properties

Another way to customize your Maven project is to use the properties of Maven and its plugins. Whereas goals describe how and in what order scripts are executed, properties customize the environment, sources, and targets. Goals and properties go hand in hand, and you will find the description of goals along with the properties on a plugin web site such as that of the .war plugin.

Note: I would recommend that you only change properties when absolutely necessary, and keep the pre-set properties of Maven whenever possible. This will reduce your workload and keep various projects similarly structured.

Maven properties are first set by a list of properties files, in which the last definition wins (please check out Maven's web site for more documentation). Afterwards, plugins can set values but are not allowed to override them, meaning your settings will prevail. Since this also applies to your extension script, I recommend setting values in a properties file. Finally, any properties set with the -D option when Maven is started will override any previously set values.

Maven Tips and Tricks

This should be enough introduction for us to start looking into various tips and tricks how to use Maven extensively, so let's begin.

Extending a Given Goal

To test class loaders, I had to create multiple archives with the same fully qualified class name but with different code. This meant my code could not be compiled in one step, as Maven does by default. The goal to run the test is test:test, and so I needed to create the archive first. To do this, maven.xml contains a piece of code that looks like this:

<preGoal name="test:test">
   <attainGoal
      name="build-test-archives"/>
</preGoal>
<postGoal name="test:test">
   <attainGoal name="do-cleanup"/>
</postGoal>
<!--AS NOTE: Build the various archives
       needed for the unit tests -->
<goal name="build-test-archives">
   <!-- Do the magic here -->
</goal>
...

To make things easier to read, I normally create private goals instead of putting the code into the preGoal tag, but that is up to you. Inside of the private goal, I compile the code and create the archives. This is basically good old Ant scripting:

<ant:mkdir
   dir="${maven.build.dir}/test-classes/base"/>
<ant:javac 
destdir="${maven.build.dir}/test-classes/base"
   debug="on"
   srcdir="${basedir}/test/src/java.base"
>
   <ant:classpath>
      <ant:pathelement
path="${maven.build.dest}"/>
      <ant:pathelement
path="${maven.build.dir}/../../util/target/classes"/>
   </ant:classpath>
</ant:javac>
<ant:mkdir
   dir="${maven.build.dir}/test-archives"/>
<ant:jar
   jarfile=
"${maven.build.dir}/test-archives/test.base.jar"
   basedir=
"${maven.build.dir}/test-classes/base"
>
   <ant:manifest>
      <ant:attribute
name="Built-By" value="${user.name}"/>
      <ant:attribute
name="Created-By" value="maven"/>
      <ant:attribute
name="Package" value="${pom.package}"/>
   </ant:manifest>
   <ant:include name="**/*.class"/>
   <ant:exclude
name="**/ReferenceClass.class"/>
</ant:jar>

Using Maven's Reactor for Multiple Projects

With JDoppio, I wanted to do the opposite of JBoss' habit of making everything so tightly integrated that sub-projects cannot be used by themselves. Therefore, I wanted to create sub-projects that could be built without building the entire project, but still able to build the entire project in one step. Maven's reactor tag enables the developer to call a specified goal on every project found, identified by a POM, and so new projects can be added later on without breaking the build. The reactor is defined in the maven.xml file as part of another goal:

<goal name="jdoppio:clean"
      description="Clean all JDoppio
         components" prereqs="clean">
   <maven:reactor basedir="${basedir}"
          includes="**/project.xml"
          goals="clean:clean"
          banner="Cleaning:"
          ignoreFailures="false" />
</goal>

This goal calls the clean:clean goal on every POM found in the project's subdirectories, so that the next time the project is built, it will be created from scratch. Note: because the prerequisite of the goal itself is the goal clean, the main project's own clean goal is executed first. There is also a multi-project plug in available, but this is for generating a project web site composed of multiple sub-projects.

Dependency on Local Archives

The sub-projects would not be of much help if they couldn't be used within other sub-projects. This can be done in multiple ways. A project could copy the generated file from another subproject. Or it could copy its generated archives to a directory in the main project. In my case, I used the local repository because with that, I could specify the dependency in the POM with an explicit version. The only drawback is that the sub-projects that another project depends upon must be built beforehand locally at least once, otherwise it will fail. The dependency looks like this:

<dependency>
   <groupId>jdoppio</groupId>
   <artifactId>jdoppio.loader</artifactId>
   <version>0.1</version>
</dependency>

This dependency states that an archive is looked for in jdoppio/jars with the name jdoppio.loader-0.1.jar. To have an archive like this available in the local repository, the subproject POM must look like this:

<project>
...
   <id>jdoppio.loader</id>
   <name><what ever you like></name>
   <groupId>jdoppio</groupId>
...

Marking Dependencies for a Particular Usage

A nice feature of Maven is that its POM is accessible from a Jelly script, and so it is possible to loop through the dependencies and check them out. In the .war plugin it is possible to indicate that an archive has to be added to the .war file (into the /WEB-INF/lib directory) by adding a war.bundle property like this:

<dependency>
   <groupId>jdoppio</groupId>
   <artifactId>jdoppio.loader</artifactId>
   <version>0.1</version>
   <properties>
      <war.bundle>true</war.bundle>
   </properties>
</dependency>

I was facing the problem of copying some of the archives in the POM's dependencies list to library directory of a test server installation, to run the server standalone. So the script loops through the dependencies, looks for a particular property, and checks the type of the dependency, and when everything is okay, it will copy it:

<!-- Loop through all artifacts -->
<j:forEach var="lib"
      items="${pom.artifacts}">
   <j:set var="dep"
      value="${lib.dependency}"/>
   <!-- Check if a certain property is set
   correctly and that the archive is of the
   correct type -->
   <j:if
test="${dep.getProperty('server.lib')=='true'}">
      <j:if test="${dep.type =='jar'}"> 
         <!-- Now we can copy this archive -->
         <ant:copy
todir="${maven.build.dir}/test-server/lib"
file="${lib.path}"/>  
      </j:if> 
   </j:if>
</j:forEach>

Looping Through a Directory

After being told that JAXB archives can be freely distributed, I needed to add them into the project directory and add them to a class path, so that JAXB could generate the XML binding classes and compile these classes afterwards. One of the Jelly's Ant tags is a File Scanner, which allows you to obtain an iterator over a list of files and then use them within your maven.xml file. In the Maven tag library, the tag addPath allows you to add a path to an existing path. So here I loop over the /lib directory and add them to a class path. Note: I ran into a problem with using dots in the name of the class path which could only be resolved after omitting the dots (I guess Maven is treating this as an attribute).

<!-- Note the capital S because it seems to be
   case sensitive here -->
<ant:fileScanner var="ownLibs">
   <ant:fileset dir="${basedir}/lib">
      <ant:include name="*.jar"/>
   </ant:fileset>
</ant:fileScanner>
<!-- A file scanner provides and iterator so we
        this one in the forEach to loop over the
        directory -->
<j:forEach var="entry"
items="${ownLibs.iterator()}">
   <!-- Create a path to be added afterwards
           to the dependency class path -->
   <ant:path id="libEntryPath"
      path="${entry}"/>
   <maven:addPath
id="maven.dependency.classpath"
refid="libEntryPath"/>
</j:forEach>

Also note that I use the maven.dependency.classpath property to add the archives. The reason to do so is that the Java plugin is using this as the class path to compile the project's Java classes, which is necessary because the project is using JAXB classes such as JAXBException. This is an example of customizing Maven rather than scripting the compilation yourself, which saves you time and money. In the case of using your own path, you need to create it up front with <and:path id="<Your Name>"/>, because the path has to exist when used in addPath.

I converted this code into a Maven plugin that can be downloaded from my weblog. This plugin comes with the current JAXB archives and does not need any additional archives to be downloaded.

Checking the Presence of a File or Directory

In the Catalina service, I wanted to automate the extraction of the Catalina distribution, but only do so if this had not already been done. So I needed to figure out whether the directory already existed:

<-- Create a property with a default value so
       that property is set even when directory is
       not available -->
<j:set var="catalinaPresent"
   value="false"/>
<-- Line below is wrapped to fit the page -->
<util:available file=
"${maven.build.dir}/../../../../
server/target/test-server/catalina"
>
   <-- Directory is available not set the
      property value to true -->
   <j:set var="catalinaPresent"
      value="true"/>
</util:available>
<-- If property is not set to true so we need
   to extract Catalina -->
<j:if
   test="${catalinaPresent != 'true'}">
   <ant:unzip
...

Project Web Sites

Maven can create a project web site with quite a number of reports in it -- that's the reason so many project web sites look similar. Again by default Maven is using its own template to create the web site, but this can be customized by providing a navigation.xml file in the /xdocs directory of your project. In my case, I wanted to include the sub-projects as well as additional information to the project web site. This file can look like this:

<project name="JDoppio">
   <title>JDoppio: The Next Generation
Application Server</title>
   <body>
      <links>
         <item name="Maven"
href="http://maven.apache.org/"/>
      </links>
      <menu name="Definition">
         <item name="Download"
href="download.html"/>
...
      </menu>
      <menu name="Projects">
         <item name="Utility Project"
href="./multiproject/jdoppio.util/index.html"/>
...
      </menu>
   </body>
</project>

First, I added a link back to Maven as a tribute to them. Then I created a menu, Definition, with links to other web pages, such as the download instruction page shown here. Afterwards, in the Projects menu, I added a link to a subproject, Util, so that the user can easily navigate to the project web site of this subproject. Within the /xdocs directory, all pages are written in the xdoc format with an .xml extension and the XDoc plugin will generate HTML pages from them. In JDoppio I am using the Multi-Project plugin to generate the entire project web site, because the project is composed of multiple sub-projects. Every subdirectory is placed into the common /multiproject directory and therefore, a link to a subproject in navigation page is just relative to the /multiproject directory.

Documentation and Help

The best documentation and help for Maven I've found so far are the Maven plugins. They are a great source of ideas on how to use Maven, even though it is sometimes tough to distinguish between the old and new styles. It also helps to check out the tag library documentation on the Jelly web site if you need to know more about Jelly and its tags. The Maven Jelly tags can be found on the Maven web site. You could also search the web for projects using Maven, like my JDoppio project, and check out how they are building their projects.

Conclusion

I like Maven because it lets me focus on what I want to accomplish and because I can avoid dealing with nasty build scripts that Ant makes me write over and over again. So far, I have not run into any hurdles that would make me question my decision to use Maven. Nevertheless, I do recommend starting slowly with Maven and letting your knowledge and confidence grow with it. I would like it if Ant would provide a Maven task, so that developers could gradually migrate their projects from Ant to Maven.

Finally, my last tip: let Maven run the project and limit yourself to steering Maven instead of controlling it. In Ant, developers control the build process with their scripts, but in Maven, there is a building process already in place, so it is much easier to go with it. Most of the time, the solution is just around the corner and only needs to be utilized the right way.

Have fun!

Andreas Schaefer is a system architect for J2EE at SeeBeyond Inc., where he leads application server development.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.