22 Sep 04

One Build File, Many Deployment Environments

Daniel Frey sent in this report describing how he uses Ant’s immutable properties and code generation to build a J2EE application for any given deployment environment at the push of a button:

I was looking for a way to manage automation for a web-based, J2EE system deployed into development, QA, pre-production, and production environments, each of which uses a WebLogic application server and an Oracle database. My biggest concern was the configuration data for the database and application server. A "push button" build would only be possible for one environment without some way to parameterize the configuration data. Ant has facilities to aid in this, so I expanded on what I read in Pragmatic Project Automation to help me achieve this cross-environment deployment automation.

Before describing how I created my "push button" build, let me briefly explain our system. It’s a J2EE-based application in a tiered architecture. Persistence for the proof of concept was achieved using a MySQL database. Hibernate is providing the O/R mapping capabilities and the Spring framework is wiring all of this up together. Hibernate provides Ant tasks for generating .java files and building the database structure from Hibernate XML files. I am also using DBUnit as a means of populating test data.

I created my project directory structure as defined in Pragmatic Project Automation with a couple of minor additions. First, I added a config directory to the root of my project directory structure. The config directory contains various configuration files— properties files and XML files—for the deployment environment. These configuration files contain place holders (tokens) for deployment information such as what database to use. The tokens are replaced with actual values when Ant copies the configuration files to the build/prod directory. Here is the config Ant target that performs the copy:

  <target name="config" depends="prepare">

    <copy todir="${build.prod.dir}">
      <fileset dir="${config.dir}">
        <include name="*.properties" />
        <include name="*.xml" />
      </fileset>
      <filterset begintoken="%" endtoken="%">
        <filter token="HIBERNATE.DIALECT" value="${hibernate.dialect}" />
        <filter token="HIBERNATE.DRIVER" value="${hibernate.connection.driver_class}" />
        <filter token="HIBERNATE.URL" value="${hibernate.connection.url}" />
        <filter token="HIBERNATE.USER" value="${hibernate.connection.username}" />
        <filter token="HIBERNATE.PASSWORD" value="${hibernate.connection.password}" />
      </filterset>
    </copy>

  </target>

This copies all of the configuration files in the config directory to the build/prod directory. As the files are copied, the filterset replaces all of the tokens observed in the configuration files with the appropriate values. All of the token values are supplied in one properties file that I specify when Ant runs the build file. For example, when I want to deploy into an environment that uses MySQL, I use a properties file that contains these key/value pairs:

  hibernate.dialect=net.sf.hibernate.dialect.MySQLDialect
  hibernate.connection.driver_class=com.mysql.jdbc.Driver
  hibernate.connection.url=jdbc:mysql://server/database
  hibernate.connection.username=user
  hibernate.connection.password=password

Using this file, any configuration file in the config directory that contains the token %HIBERNATE_USER%, for example, will be changed to reference the value user. I have a default properties file, but I can change that via the command line when Ant is run. For example, if I want to deploy to the QA environment, I use:

  $ ant -propertyfile qa.properties

I can also override specific properties on the command line. For example, to override the value of hibernate.connection.username, I’d type:

  $ ant -Dhibernate.connection.username=admin -propertyfile qa.properties

I also added a stage directory to my build directory to store the .java files generated by Hibernate. I needed a separate staging area for these files because I did not want them to be in the src directory. These are generated files and can be re-generated at any given time by executing the codegen target:

  <target name="codegen" depends="config">

    <taskdef name="hbm2java"
             classname="net.sf.hibernate.tool.hbm2java.Hbm2JavaTask"
             classpathref="project.classpath" />

    <hbm2java output="${build.stage.dir}">
      <fileset dir="${src.dir}">
        <include name="**/*.hbm.xml" />
      </fileset>
    </hbm2java>

    <copy todir="${build.prod.dir}">
      <fileset dir="${src.dir}">
        <include name="**/*.hbm.xml" />
      </fileset>
    </copy>

  </target>

The hbm2java task generates the .java files into the build/stage directory from the Hibernate .hbm.xml files in the src directory. The <copy> task just moves the .hbm.xml files to the build/prod directory so that they are available on the classpath for the Spring framework to find.

After all the configuration is complete and the .java files are generated, Ant then compiles everything:

  <target name="compile" depends="codegen">
    <javac srcdir="${build.stage.dir}" destdir="${build.prod.dir}">
      <classpath refid="project.classpath" />
    </javac>
    <javac srcdir="${src.dir}" destdir="${build.prod.dir}">
      <classpath refid="project.classpath" />
    </javac>
  </target>

However, I don’t run my tests right away. Hibernate must first generate and execute the DDL to the MySQL database in order to set up the database structure:

  <target name="schema" depends="compile">

    <taskdef name="schemaexport"
             classname="net.sf.hibernate.tool.hbm2ddl.SchemaExportTask"
             classpathref="project.classpath" />

    <schemaexport properties="${build.prod.dir}/hibernate.properties"
      quiet="no" text="no" drop="no" delimiter=";">
      <fileset dir="${build.prod.dir}">
        <include name="**/*.hbm.xml" />
      </fileset>
    </schemaexport>

  </target>

Then I use DBUnit to populate my test data:

  <target name="setupDB" depends="schema">

    <taskdef name="dbunit"
             classname="org.dbunit.ant.DbUnitTask">
      <classpath refid="project.classpath" />
    </taskdef>

    <dbunit driver="${hibernate.connection.driver_class}"
            url="${hibernate.connection.url}"
            userid="${hibernate.connection.username}"
            password="${hibernate.connection.password}">
      <operation type="CLEAN_INSERT" src="db/TestData.xml" />
    </dbunit>

  </target>

Finally, Ant runs the test target to compile and execute my tests. With these build targets chained together as I’ve described, I can execute the test target and everything is run at the push of a button.

This has proved to be very useful and successful every time I execute the build script. It also provides a number of advantages. First, I can execute this on each of my systems, changing only the properties file referenced on the command line. Second, I can easily change the target database because I am using Hibernate to provide O/R mapping capabilities. To accomplish this, all I have to do is change the value of the Hibernate properties in one properties file or on the command line when Ant is run. I have successfully done this between MySQL and HSQLDB.