Maven is the EJB2 of build tools

KnowledgePlaza has a rapid development platform called Cheyenne. Cheyenne is based on Java and produces in the end a WAR file, that can be run inside any servlet 2.5 / jsp 2.1 compatible container (like Tomcat 6). Cheyenne projects are build using Maven.

One of the features in the build is that there is the option of overlays; a Cheyenne project can be overlaid with other Cheyenne projects, who provide generic Java classes, JSP files, resources, etc. This feature is very similar to the overlay feature in the default WAR plugin, but Cheyenne has a few tweaks and differences that have forced us to write our own Maven plugin.

Initially the concept of Maven is very straight forward; there is a default build cycle consisting of about 20 steps (phases), and certain actions are bound to the phases based on the packaging type. For example: building JARs require different actions than building WARs. Simple? Simple. So in order to implement our tweaked build for Cheyenne all that needs to be done is write some custom actions and bind them to the build cycle. But there is this huge gap between theory and actually getting it to work.

First step is these custom actions. As it happens we required just some tweaks on the standard WAR plugin, and since the plugins are written in Java it should be simple to extend the existing plugin… Unfortunately not. Maven plugins use pre-Java5 annotations, which are placed in the comment above variables and methods, similar to the Javadoc annotation. Like this:

    /**
     * The maven project.
     *
     * @parameter expression="${project}"
     * @required
     * @readonly
     */
    private MavenProject project;

Only these are not Javadoc annotations but real Java wannabe-annotations. When building the plugin, the Maven build process analyses the Java source file and extracts these annotations and uses it for all kinds of things, including dependency injection as the example above shows. This is the way annotations were done before Java supported annotations itself, before Java 1.5. Biggest drawback? The annotations are not part of the class file and that is why extending them will not work. So instead of extending the existing WAR plugin, the actual source code must be included into the project (hurray for open source). Point is; annotations have been around since Java 1.5, since 2004, and Java 1.7 is about to be released (seriously delayed)…

Then binding the actions to the build process. Normally you would do this using plugins in the pom.xml, like so:

	<plugins>
		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-compiler-plugin</artifactId>
			<version>2.3.1</version>
			<configuration>
				<source>1.6</source>
				<target>1.6</target>
				<showDeprecation>false</showDeprecation>
				<warning>false</warning>
				<fork>false</fork>
			</configuration>
		</plugin>
	</plugins>

Loads of XML to configure stuff, very EJB2. I always wondered why this was not written more compact, something like:

	<plugins>
		<maven-compiler-plugin groupId="org.apache.maven.plugins" version="2.3.1" source="1.6"
			target="1.6" showDeprecation="false" warning="false" fork="false"
	</plugins>

But this particular plugin also exposes another issue with Maven; bad defaults. The Maven 2 build process always assumes Java 1.2, Maven 3 per default assumes Java 1.5, but it still is a bad default. Using the currently used javac’s would be the smarter choice.

Back to the binding. In order to prevent every Cheyenne project to have to include four plugin XML sniplets in their poms (making them hard to maintain), one either lets the project pom inherit from a special Cheyenne superpom, or a custom build cycle is required. Since we do not want to limit the dynamics of the project pom by forcing a packaging-related parent to be inherited, the custom build cycle approach was used. This means that only a single Cheyenne plugin XML sniplet is still required in the project’s pom, but with one special setting:

	<plugin>
		<groupId>nl.innovationinvestments</groupId>
		<artifactId>CheyenneMavenPlugin</artifactId>
		<version>1.11</version>
		<extensions>true</extensions>
	</plugin>

The extentions tag is the key; it tells Maven that the plugin actually holds a whole new build cycle mapping. This build cycle contains the actual mapping of all the actions to the build cycle phases. Naturally this involves yet another XML configuration:

<?xml version="1.0"?>
<component-set>
  <components>
	<!-- this is the custom life cycle binding -->
    <component>
      <role>org.apache.maven.lifecycle.mapping.LifecycleMapping</role>
      <role-hint>chy</role-hint>
      <implementation>org.apache.maven.lifecycle.mapping.DefaultLifecycleMapping</implementation>
      <configuration>
        	<lifecycles>
          	<lifecycle>
            <id>default</id>
            <phases>
              <generate-sources>org.appfuse:maven-warpath-plugin:2.0.2:add-classes</generate-sources> <!-- also include dependencies in war dependencies -->
              <generate-sources>nl.innovationinvestments:CheyenneMavenPlugin:${project.version}:compile</generate-sources> <!-- call the cheyenne compiler -->
              <process-resources>org.apache.maven.plugins:maven-resources-plugin:resources</process-resources>
              <compile>org.apache.maven.plugins:maven-compiler-plugin:compile</compile>
              <process-test-resources>org.apache.maven.plugins:maven-resources-plugin:testResources</process-test-resources>
              <test-compile>org.apache.maven.plugins:maven-compiler-plugin:testCompile</test-compile>
              <test>org.apache.maven.plugins:maven-surefire-plugin:test</test>
              <package>nl.innovationinvestments:CheyenneMavenPlugin:${project.version}:package</package> <!-- call the cheyenne packager -->
              <install>org.apache.maven.plugins:maven-install-plugin:install</install>
              <deploy>org.apache.maven.plugins:maven-deploy-plugin:deploy</deploy>
            </phases>
          </lifecycle>
        </lifecycles>
      </configuration>
    </component>

	<!-- this makes sure we generate the correct type of artifacts -->
    <component>
      <role>org.apache.maven.artifact.handler.ArtifactHandler</role>
      <role-hint>chy</role-hint>
      <implementation>
        org.apache.maven.artifact.handler.DefaultArtifactHandler
      </implementation>
      <configuration>
        <type>chy</type>
        <extension>war</extension>
        <language>java</language>
        <addedToClasspath>true</addedToClasspath>
      </configuration>
    </component>

	<!-- copied from warpath plugin -->
	<component>
      <role>org.apache.maven.artifact.handler.ArtifactHandler</role>
      <role-hint>warpath</role-hint>
      <implementation>org.apache.maven.artifact.handler.DefaultArtifactHandler</implementation>
      <configuration>
        <type>war</type>
        <includesDependencies>false</includesDependencies>
        <language>java</language>
        <addedToClasspath>false</addedToClasspath>
      </configuration>
    </component>
   <component>
    <role>org.apache.maven.artifact.handler.ArtifactHandler</role>
    <role-hint>classes</role-hint>
    <implementation>org.apache.maven.artifact.handler.DefaultArtifactHandler</implementation>
    <configuration>
      <type>war</type>
      <includesDependencies>true</includesDependencies>
      <language>java</language>
      <addedToClasspath>true</addedToClasspath>
    </configuration>
  </component>
  <component>
    <role>org.apache.maven.artifact.handler.ArtifactHandler</role>
    <role-hint>war</role-hint>
    <implementation>org.apache.maven.artifact.handler.DefaultArtifactHandler</implementation>
    <configuration>
      <type>war</type>
      <includesDependencies>false</includesDependencies>
      <language>java</language>
      <addedToClasspath>false</addedToClasspath>
    </configuration>
  </component>

  </components>
</component-set>

Using this approach meant that we could no longer use the warpath plugin (which was one of the XML plugins), because it also required the use of “extention”, and only one build cycle can be active at any time. That again meant we had to import the binding of the warpath plugin into our own. To be frank, I gave up trying to understand what exactly is being configured here. Too much XML, too much levels, too much EJB2 / struts. It worked and that was enough time spent.

To summarize; the current Maven (3.0) gives me XML blues just like EJB2 and struts did. Don’t get me wrong, I like Maven. I use it for all my projects, because of its many advantages. And the concept is fine, only the implementation is too complex due to too many layers and configuration files. Coding-by-XML is not the way to go. My wishlist for Maven 3.1 would have it be more like EJB 3, by:
– more compact poms and notation style
– smarter more dynamic defaults
– use Java annotations when developing plugins, thus allowing extending them
– using less XML for plugin development, maybe even no XML at all for 90% of the use cases
– allow plugins to hook into multiple phases which just one entry in the project’s pom
– allow multiple plugins to do this simultaneously

Afterall it’s been about 5 years since EJB3 was released and that was the “corporate slow” adoption of the keep-it-simple style. If a huge and heavy process like the JCP adopted this style 5 years ago…

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.