Over a million developers have joined DZone.

Maven: Throwing Out the Bath Water, Keeping the Baby

· Java Zone

Discover how AppDynamics steps in to upgrade your performance game and prevent your enterprise from these top 10 Java performance problems, brought to you in partnership with AppDynamics.

... in other words, a first step towards using Maven for dependency management but NOT builds. That's the irony of Maven ... they've conflated two things (dependency management and builds) in such as way that they make the useful one (dependency management) painful because the build system is so awful.

As an interrum step between full Maven and (most likely) Gradle, I've been looking at a way to use Maven for dependencies only in a way that is compatible with Eclipse ... without using the often flakey and undependable M2Eclipse plugin.

In any case, rather than assuming that dependencies might change at any point in time at all, let's assume that when I change dependencies (by manually editing pom.xml) I know it and am willing to run a command to bring Eclipse (and my Ant-based build) in line.

First, my pom.xml:

<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
  xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.example</groupId>
  <artifactId>myapp</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <dependencies>
    <dependency>
      <groupId>org.apache.tapestry</groupId>
      <artifactId>tapestry-hibernate</artifactId>
      <version>${tapestry-version}</version>
      <scope>compile</scope>
      <exclusions>
        <exclusion>
          <artifactId>log4j</artifactId>
          <groupId>log4j</groupId>
        </exclusion>
        <exclusion>
          <artifactId>slf4j-log4j12</artifactId>
          <groupId>org.slf4j</groupId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>org.apache.tapestry</groupId>
      <artifactId>tapestry-test</artifactId>
      <version>5.2.0-SNAPSHOT</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>servlet-api</artifactId>
      <version>2.4</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.lucene</groupId>
      <artifactId>lucene-core</artifactId>
      <version>2.4.1</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-search</artifactId>
      <version>3.1.1.GA</version>
    </dependency>
    <dependency>
      <groupId>commons-lang</groupId>
      <artifactId>commons-lang</artifactId>
      <version>2.4</version>
    </dependency>
    <dependency>
      <groupId>commons-beanutils</groupId>
      <artifactId>commons-beanutils</artifactId>
      <version>1.8.0</version>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>0.9.17</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.5.8</version>
      <type>jar</type>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>postgresql</groupId>
      <artifactId>postgresql</artifactId>
      <version>8.4-701.jdbc4</version>
    </dependency>
    <dependency>
      <groupId>xerces</groupId>
      <artifactId>xercesImpl</artifactId>
      <version>2.4.0</version>
      <type>jar</type>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>com.howardlewisship</groupId>
      <artifactId>tapx-datefield</artifactId>
      <version>${tapx-version}</version>
    </dependency>
    <dependency>
      <groupId>com.howardlewisship</groupId>
      <artifactId>tapx-prototype</artifactId>
      <version>${tapx-version}</version>
    </dependency>
    <dependency>
      <groupId>org.testng</groupId>
      <artifactId>testng</artifactId>
      <version>5.9</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <repositories>
    <repository>
      <id>tapestry360-snapshots</id>
      <url>http://tapestry.formos.com/maven-snapshot-repository/</url>
    </repository>
    <repository>
      <id>repository.jboss.org</id>
      <url>http://repository.jboss.org/maven2/</url>
    </repository>
  </repositories>
  <properties>
    <tapestry-version>5.1.0.5</tapestry-version>
    <lucene-version>2.4.1</lucene-version>
    <tapx-version>1.0.0-SNAPSHOT</tapx-version>
  </properties>
</project>

(This was adapated from one of my client's POMs).

Next, an Ant build file that compiles this, runs tests and builds a WAR:

<project name="example" xmlns:mvn="urn:maven-artifact-ant">

  <property name="classes.dir" value="target/classes" />
  <property name="test.classes.dir" value="target/test-classes" />
  <property name="web.lib.dir" value="target/web-libs" />
  <property name="webapp.dir" value="src/main/webapp" />
  <property name="webinf.dir" value="${webapp.dir}/WEB-INF" />

  <path id="compile.path">
    <fileset dir="lib/provided" includes="*.jar"/>
    <fileset dir="lib/runtime" includes="*.jar" />
  </path>

  <path id="test.path">
    <path refid="compile.path" />
    <pathelement path="${classes.dir}" />
    <fileset dir="lib/test" includes="*.jar" />
  </path>

  <target name="clean" description="Delete all derived files.">
    <delete dir="target" quiet="true" />
  </target>

  <!-- Assumes that Maven's Ant library is installed in ${ANT_HOME}/lib/ext. -->

  <target name="-setup-maven">
    <typedef resource="org/apache/maven/artifact/ant/antlib.xml" uri="urn:maven-artifact-ant" />
    <mvn:pom id="pom" file="pom.xml" />
  </target>

  <macrodef name="copy-libs">
    <attribute name="filesetrefid" />
    <attribute name="todir" />
    <sequential>
      <mkdir dir="@{todir}" />
      <copy todir="@{todir}">
        <fileset refid="@{filesetrefid}" />
        <mapper type="flatten" />
      </copy>
    </sequential>
  </macrodef>

  <macrodef name="rebuild-lib">
    <attribute name="base" />
    <attribute name="scope" />
    <attribute name="libs.id" default="@{base}.libs" />
    <attribute name="src.id" default="@{base}.src" />
    <sequential>
      <mvn:dependencies pomrefid="pom" filesetid="@{libs.id}" sourcesFilesetid="@{src.id}" scopes="@{scope}" />
      <copy-libs filesetrefid="@{libs.id}" todir="lib/@{base}" />
      <copy-libs filesetrefid="@{src.id}" todir="lib/@{base}-src" />
    </sequential>
  </macrodef>

  <target name="refresh-libraries" depends="-setup-maven" description="Downloads runtime and test libraries as per POM.">
    <delete dir="lib" quiet="true" />
    <rebuild-lib base="provided" scope="provided"/>
    <rebuild-lib base="runtime" scope="runtime,compile" />
    <rebuild-lib base="test" scope="test" />
    <echo>
      
*** Use the rebuild-classpath command to update the Eclipse .classpath file.</echo>
  </target>

  <target name="compile" description="Compile main source code.">
    <mkdir dir="${classes.dir}" />
    <javac srcdir="src/main/java" destdir="${classes.dir}" debug="true" debuglevel="lines,vars,source">
      <classpath refid="compile.path" />
    </javac>
  </target>

  <target name="compile-tests" depends="compile" description="Compile test sources.">
    <mkdir dir="${test.classes.dir}" />
    <javac srcdir="src/test/java" destdir="${test.classes.dir}" debug="true" debuglevel="lines,vars,source">
      <classpath refid="test.path" />
    </javac>
  </target>

  <target name="run-tests" depends="compile-tests" description="Run unit and integration tests.">
    <taskdef resource="testngtasks" classpathref="test.path" />
    <testng haltonfailure="true">
      <classpath>
        <path refid="test.path" />
        <pathelement path="${test.classes.dir}" />
      </classpath>

      <xmlfileset dir="src/test/conf" includes="testng.xml" />

    </testng>
  </target>


  <target name="war" depends="run-tests,-setup-maven" description="Assemble WAR file.">

    <!-- Copy and flatten the libraries ready for packaging. -->

    <mkdir dir="${web.lib.dir}" />
    <copy todir="${web.lib.dir}" flatten="true">
      <fileset dir="lib/runtime" />
    </copy>
    <jar destfile="${web.lib.dir}/${pom.artifactId}-${pom.version}.jar" index="true">
      <fileset dir="src/main/resources" />
      <fileset dir="${classes.dir}" />
    </jar>

    <war destfile="target/${pom.artifactId}-${pom.version}.war">
      <fileset dir="${webapp.dir}" />
      <lib dir="${web.lib.dir}" />
    </war>
  </target>
</project>

The key target here is refresh-libraries, which deletes the lib directory then repopulates it. It creates a sub folder for each scope (lib/provided, lib/runtime, lib/test) and another sub folder for source JARs (lib/provided-src, lib/runtime-src, etc.).

So how does this help Eclipse? Ruby to the rescue:

#!/usr/bin/ruby
# Rebuild the .classpath file based on the contents of lib/runtime, etc.

# Probably easier using XML Generator but don't have the docs handy

def process_scope(f, scope)
 # Now find the actual JARs and add an entry for each one.
 
 dir = "lib/#{scope}"

 return unless File.exists?(dir)
 
 Dir.entries(dir).select { |name| name =~ /\.jar$/ }.sort.each do |name|
   f.write %{  <classpathentry kind="lib" path="#{dir}/#{name}"}
   
   srcname = dir + "-src/" + name.gsub(/\.jar$/, "-sources.jar") 

   if File.exist?(srcname)
      f.write %{ sourcepath="#{srcname}"}
   end
   
   f.write %{/>\n}
 end
end

File.open(".classpath", "w") do |f|
  f.write %{<?xml version="1.0" encoding="UTF-8"?>
<classpath>
  <classpathentry kind="src" path="src/main/java"/>
  <classpathentry kind="lib" path="src/main/resources"/>
  <classpathentry kind="src" path="src/test/java"/>
  <classpathentry kind="lib" path="src/test/resources"/>
  <classpathentry kind="output" path="target/classes"/>
  <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
}

 process_scope(f, "provided")
 process_scope(f, "runtime")
 process_scope(f, "test")
 
 f.write %{
</classpath>
}
    
end

That's pretty good for half an hour's work. This used to be much more difficult (in Maven 2.0.9), but the new scopes attribute on the Maven dependencies task makes all the difference.

Using this you are left with a choice: either you don't check in .classpath and the contents of the lib folder, in which case you need to execute the target and script in order to be functional ... or you simply check everything in. I'm using GitHub for my project repository ... the extra space for a few MB of libraries is not an issue and ensures that I can set up the exact classpath needed by the other developers on the project with none of the usual Maven guess-work. I'm looking forward to never having to say "But it works for me?" or "What version of just about anything do you have installed?" or "Try a clean build, and close and reopen your project, and remove and add Maven dependency support, then sacrifice a small goat" every again.

Next up? Packaging most of this into an Ant library so that I can easily reuse it across projects ... or taking the time to learn Gradle and let it handle most of this distracting garbage.

From http://tapestryjava.blogspot.com/

The Java Zone is brought to you in partnership with AppDynamics. AppDynamics helps you gain the fundamentals behind application performance, and implement best practices so you can proactively analyze and act on performance problems as they arise, and more specifically with your Java applications. Start a Free Trial.

Topics:

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}