Wednesday, November 10, 2010

Using Ant with NatJet

In this post, I will present a quite standard Ant build file for a NatJet project. Notice, that as NatJet is a standard Dynamic Web Project in Eclipse, this will also work for any web project having the same structure.

A NatJet project has by default the following structure :

  • src : source directory
  • build : a temporary directory where class files are generated
  • WebContent : a directory that stores files that will be deployed on the web server

I won’t present Ant : I will use it as an automate for integration.

Integration

For me, the main goal of integration is to insure consistency between source and binary. It is not just producing a binary for production : integration has to be the guardian of this consistency.

Second point, I’m using the Run As –> On server menu to test while in development. Ant will only be used in the integration process to deliver a war to a customer for test or production. This means that my configuration is not optimized to speed integration but to insure the consistency.

The way I will use Ant, is directly inspired by this quest. This is important because each solution has strengths and weaknesses : I will therefore be more interested in strength that insure consistency than speed.

Delivery for test or production brings generally some new constraints :

  • database connection is not the same as in development
  • sometimes there are other parameters or some specific constraints to the production environment

In my sample, I will have two specificities to handle :

  1. for database connection, I’m using a datasource which is defined in the context.xml file that is in META-INF repository. I need two different files : one for development one for production. There is no need to insure a consistency between the two files.
  2. as I’m using a datasource, the hibernate.cfg.xml file needs the DTD declaration. This declaration is a problem : either I use an URL but in this case the server needs a internet access or I put a file with a relative path, but in this case the path has to be changed for every server. Anyway, I can’t use the same declaration for development and production. The hibernate.cfg.xml file needs to be the same between both environments : there is only one line that needs to change : the DTD declaration. A hibernate file may evolve quite a lot. In this case, I do not want to have to maintain two files.

Functionalities of my ant build.xml

As I’ve stated it before, the default task of my build.xml will be the the war task that build the war file. The default configuration will correspond to the one of the production environment. There will be defined in the build.properties file.

If you are in a hurry and quite familiar with ant, you may prefer to skip this section and go directly to the two files :Summary : the two files

The sample build.xml ant file will also :

  1. exclude the java files for the jUnit test from the compile task
  2. replace a line in the hibernate.cfg.xml file depending on the configuration
  3. pick up the right context.xml file
  4. exclude xml files corresponding to the panel description

Finally, I have define two ant task to modify file according to the environment

Default task to build a war

The first tag is project tag. This tag defines :

  • project name : It’s just for me a way to attach the build.xml file to a project
  • basedir : It’s usually “.” meaning the root of the eclipse project
  • default : this is the default task, that is highlighted. In my case is will be war. Pepel using ant in the development process use build or compile task as a default.

In my case, it will be :

<project name="my-natjet-project" basedir="." default="war">

As I want to insure consistency of source and binary (the war file), I will have the following chain dependency of my ant tasks :

  • <target name="clean">
  • <target name="copy-non-java-files">
  • <target name="compile" depends="clean,copy-context-delivery,copy-non-java-files">
  • <target name="war" depends="compile">

This means, that when I select the war task, ant will :

  1. clean my build directory
  2. copy non java file
  3. compile
  4. build the war file

In the specified order. Thus I’m sure that any class file of the war file has been built from the last version of its source file. Consistency of source and binary is insured by the process. Notice that in some configuration, the cleaning task may be an option left to the developer. I definitely reject this option as my goal is consistency and not speed.

Exclude some java files from compilation

Your project may contains jUnit java test files. Obviously, this file are not needed in production. But they may even not compile as they need some specific jar files (jUnit jar for example).

A good habit is to group all junit java test in one package distinct from other source : in my case there where all sub package of fr.natsystem.myproject.test.

In the property part of my build.xml file, I‘ve defined the following property :

<property name="junit.package"  value="fr/natsystem/myproject/test/**" />

This property is used in the compile task :

<target name="compile" depends="clean,copy-context-delivery,copy-non-java-files">
    <echo>jUnit Package ${junit.package}</echo>
    <javac srcdir="${source-directory}" destdir="${classes-directory}" classpathref="project-classpath" excludes="${junit.package}"/>
</target>

The excludes property in the javac tag is expecting a relative exclusion path starting at the srcdir (src in our case). That means, it will be the package description where we replace dot by slash.

The last two stars (**) means that all subdirectories and there content are excluded. If you put just one star, you are limiting yourself at files in the specified directory.

Replacing a line in a config file with ant

In my case, I need two versions of my hibernate.cfg.xml file. The only difference was the DTD declaration at its beginning :

For production as I do not have an internet access :

<!DOCTYPE hibernate-configuration PUBLIC  "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
    "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

In development, as when I trigger my test from eclipse the file is not found at this position, I opted for the URL

<!DOCTYPE hibernate-configuration PUBLIC  "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
    "../webapps/myProject/hibernate-configuration-3.0.dtd">

hibernate.cfg.xml files are quite long and specific files that evolves quite a lot in the process of a development. I do not want to have to keep track of this change in two files.

The solution was to have a master hibernate.cfg.xml file called in our case hibernateAntMaster.cfg.xml. This file is the one that will be modified by developers. The DTD declaration is a tag that we will replace by an ant task.

<!DOCTYPE hibernate-configuration PUBLIC  "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
    "@@DTD-Declaration@@">

We add two tasks in the ant build.xml file :

  • clean-context : this task will delete the actual hibernate.cfg.xml file and copy the reference hibernateAntMaster.cfg.xml as hibernate.cfg.xml. At this point we will have two identical files
  • copy-context-delivery : this task we replace the line in the hibernate.cfg.xml file. This task will do some other context specific tasks that we will see later.

The second task, will put the hibernat.cfg.xml file with the correct DTD declaration.

We use a property dtdLocation that we will specify in the build.properties file.

<target name="clean-context" description="Prepare files that needs to be modified depending on environment">
    <delete file="${source-directory}/hibernate.cfg.xml"/>
    <copy file="${source-directory}/hibernateAntMaster.cfg.xml" tofile="${source-directory}/hibernate.cfg.xml"/>       
</target>

<target name="copy-context-delivery" depends="clean-context" description="Task that updates files for the target environment">
    <replace file="${source-directory}/hibernate.cfg.xml" token="@@DTD-Declaration@@" value="${dtdLocation}"/>
    <delete file="${web-directory}/META-INF/context.xml"/>
    <copy file="${web-directory}/META-INF/${contextFile}" tofile="${web-directory}/META-INF/context.xml"/>
</target>

The replace ant tag allow to define :

  • the file you want to change
  • the token you want to replace in the content of the file : in our case we use @@ to delimit it in a way that avoid collision with existing syntax.
  • the value that will be used to replace the token in the file.

Picking the right config file with ant

We have two different versions of our context.xml file :

  • one for production
  • one for development

This file have little in common. They don’t evolve a lot and modification may be specific to only one of the file.

We just have to use one or the other. In our case, we create 3 files in WebContent/META-INF :

  • context.xml : the file used in test or production
  • contextProd.xml : the version we want to use in production
  • contextDev.xml : the version we want to use in development.

To achieve, the task we use the same copy-context-delivery task seen before :

<target name="copy-context-delivery" depends="clean-context" description="Task that updates files for the target environment">
    <replace file="${source-directory}/hibernate.cfg.xml" token="@@DTD-Declaration@@" value="${dtdLocation}"/>
    <delete file="${web-directory}/META-INF/context.xml"/>
    <copy file="${web-directory}/META-INF/${contextFile}" tofile="${web-directory}/META-INF/context.xml"/
>
</target>

First we delete the context.xml file, then we copy the right context file as context.xml file.

We use a contextFile property defined in the build.properties file.

Excluding directories in the ant war task

NatJet project have a directory resources in the WebContent directory. This directory contains the xml description of the panels. These descriptions are not used in production : they are transformed as java abstract class for execution.

We want to exclude this directory from the war file. The war file is build in ant from a build directory that contains classes and a WebContent directory that will be the root of the archive. We want to be able to use directly the WebContent directory at it is the rule. We do not want to have to build a second temporary directory.

To achieve, this goal we used the exclude tag to get the following definition :

<target name="war" depends="compile">
    <mkdir dir="${build-directory}" />
    <delete file="${build-directory}/${war-file-name}" />
    <war warfile="${build-directory}/${war-file-name}" webxml="${web-xml-file}">
        <classes dir="${classes-directory}" />
       
        <fileset dir="${web-directory}">
            <!-- Need to exclude it since webxml is an attribute of the war tag above -->
            <exclude name="WEB-INF/web.xml" />
            <!-- Exclude NatJet xml files for the description of panels -->
            <exclude name="resources/**" />
            <excludesfile/>
        </fileset>
        <manifest>
            <attribute name="Built-By" value="${builder}" />
            <attribute name="Built-On" value="${build-info.current-date}" />
            <attribute name="Built-At" value="${build-info.current-time}" />
        </manifest>
    </war>
</target>

Notice the manifest section : this will build a MANIFEST.MF file that will be in the META-INF directory of the war file. This allows to keep track of the version or the build number of the war.

Ant task to configure development environment

You’ve noticed that we have to deal with two environments : production and development. Two files (context.xml and hibernate.cfg.xml) depends on the selected environment.

We build the default of our ant build.xml and build.properties file to be the production environment. This is consistent with our main goal : integration for a customer delivery.

The process may affect the eclipse development environment. Thus as we have the ant infrastructure configured for our project it is quite straightforward to add a little task to configure the development environment.

For this we duplicate the copy-context-delivery ant task into copy-context-dev. The task will do the exact same thing, but we do not want to go through picking or modifying a build.properties file we replace properties by their direct definition.

In our case, it was :

  • the value in the replace tag
  • the filename of the file to copy as context.xml file
<target name="copy-context-dev" depends="clean-context" description="Task to set the development environment, should be used to update modification of master files">
    <replace file="${source-directory}/hibernate.cfg.xml" token="@@DTD-Declaration@@" value="http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"/>
    <delete file="${web-directory}/META-INF/context.xml"/>
    <copy file="${web-directory}/META-INF/contextDev.xml" tofile="${web-directory}/META-INF/context.xml"/>
</target>

In development, to set your environment you just pick this task and then do a refresh (F5) on the project node in your project explorer.

When you modify the hibernateAntMaster.cfg.xml, you just need to run this ant task to update the hibernate.cfg.xml file.

Notice, that this approach works fine, because we usually keep development environment quite consistent among the development team and we do not need specific configuration for each developer.

Summary : the complete files

We have two files both on the root of the eclipse project at the same level :

  • build.xml
  • build;properties

To edit build.xml, it may be better for you to use the right click : Open with and pick up the Ant Editor menu. This will bring syntax coloring and code completion.

To run the task, just right click on build.xml and select the menu Run As –> Ant Build… : this will opens a dialog where you can pick up the task : either choose war if you want to build the war files for delivery, or copy-context-dev to set the development environment or refresh the hibernate.cfg.xml or context file after a modification od the corresponding master.

build.properties file

The properties specific to the project or the environment will be set in this file.

Follow, the complete file:

project-name = myWarName
tomcat-home = C:/NatJet4.0.0/thirdparty/apache-tomcat-6.0.20
builder=Nat System
#Specific environnement propeties
dtdLocation="../webapps/myProject/hibernate-configuration-3.0.dtd”

contextFile=contextProd.xml

The project-name will be the name of the war file.

build.xml file

Follow the complete file :

<?xml version="1.0" encoding="UTF-8"?>
<project name="my-natjet-project" basedir="." default="war">

    <property file="build.properties" />

    <!-- Define Web Project Environnement -->
    <property name="source-directory"  value="src" />
    <property name="classes-directory"  value="build/classes" />
    <property name="web-directory"  value="WebContent" />
    <property name="war-file-name" value="${project-name}.war" />
    <property name="web-xml-file" value="${web-directory}/WEB-INF/web.xml" />
   
    <!-- Allow to exclude jUnit testcase-->
   <property name="junit.package" value="fr/natsystem/myproject/test/**" />
   
    <fail message="You need to specify contextFile property" unless="contextFile"/>
   
    <tstamp prefix="build-info">
        <format property="current-date" pattern="d-MMMM-yyyy" locale="fr" />
        <format property="current-time" pattern="hh:mm:ss a z" locale="fr" />
        <format property="year-month-day" pattern="yyyy-MM-dd" locale="fr" />
    </tstamp>
    <property name="build-directory" value="build" />

    <path id="project-classpath">
        <fileset dir="${web-directory}/WEB-INF/lib" includes="*.jar" />
        <fileset dir="${tomcat-home}/bin" includes="*.jar" />
        <fileset dir="${tomcat-home}/lib" includes="*.jar" />
    </path>

    <target name="clean">
        <delete dir="${classes-directory}" />
        <mkdir dir="${classes-directory}" />
    </target>

    <target name="clean-context" description="Prepare files that needs to be modified depending on environment">
    <delete file="${source-directory}/hibernate.cfg.xml"/>
    <copy file="${source-directory}/hibernateAntMaster.cfg.xml" tofile="${source-directory}/hibernate.cfg.xml"/>       
</target>

<target name="copy-context-delivery" depends="clean-context" description="Task that updates files for the target environment">
    <replace file="${source-directory}/hibernate.cfg.xml" token="@@DTD-Declaration@@" value="${dtdLocation}"/>
    <delete file="${web-directory}/META-INF/context.xml"/>
    <copy file="${web-directory}/META-INF/${contextFile}" tofile="${web-directory}/META-INF/context.xml"/>

    </target>
   
    <target name="copy-context-dev" depends="clean-context" description="Task to set the development environment, should be used to update modification of master files">
    <replace file="${source-directory}/hibernate.cfg.xml" token="@@DTD-Declaration@@" value="http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"/>
    <delete file="${web-directory}/META-INF/context.xml"/>
    <copy file="${web-directory}/META-INF/contextDev.xml" tofile="${web-directory}/META-INF/context.xml"/>

    </target>
   
    <target name="copy-non-java-files">
        <copy todir="${classes-directory}" includeemptydirs="false">
            <fileset dir="${source-directory}" excludes="**/*.java" />
        </copy>
    </target>

   
    <target name="compile" depends="clean,copy-context-delivery,copy-non-java-files">
        <echo>jUnit Package ${junit.package}</echo>
        <javac srcdir="${source-directory}" destdir="${classes-directory}" classpathref="project-classpath" excludes="${junit.package}"/>
    </target>

    <target name="war" depends="compile">
        <mkdir dir="${build-directory}" />
        <delete file="${build-directory}/${war-file-name}" />
        <war warfile="${build-directory}/${war-file-name}" webxml="${web-xml-file}">
            <classes dir="${classes-directory}" />
           
            <fileset dir="${web-directory}">
                <!-- Need to exclude it since webxml is an attribute of the war tag above -->
                <exclude name="WEB-INF/web.xml" />
                <!-- Exclude NatJet xml files for the description of panels -->
                <exclude name="resources/**" />
                <excludesfile/>
            </fileset>
            <manifest>
                <attribute name="Built-By" value="${builder}" />
                <attribute name="Built-On" value="${build-info.current-date}" />
                <attribute name="Built-At" value="${build-info.current-time}" />
            </manifest>
        </war>
    </target>

</project>

Conclusion

The two files can be used quite easily with any NatJet project. You may need to adapt or delete the clean-context and copy-context-delivery. I will recommend to put in comment their content in a first step because I’m almost sure that at one point you will have to deal with the same problematic. The other parts can be used as there are.