Section 17.6. Creating Portable Build Processes


17.6. Creating Portable Build Processes

As we've already seen, using Ant for enterprise development projects raises some potential portability issues with our buildfiles. Certain targets, such as deploying an application archive to an application server, are necessarily specific to a type of J2EE server and will need to be changed if another type of application server is used to run the application. These kinds of portability issues become even more complicated when you need to support multiple development and deployment environments concurrently and are multiplied again when your project makes use of a source code repository:

  • Developer environments often vary in terms of the locations of support libraries, application server directories, and so on. This requires the editing of the build properties to suit the local development environment. If these build properties are stored in a file that's checked in with the project code in a code repository, these changes need to be made each time the project code is checked out and undone before the development changes are checked back in.

  • In typical build/test/release development processes, the software being produced needs to be deployed and executed in one of several server environments, depending on the stage of the process. These environments are typically configured in different locations and may have different internal configurations. In addition, the application itself may need to be configured differently depending on the deployment environmentlocations of resource files, cache and other memory sizes, and the like may need to be set differently in different environments. Working around this issue involves the editing of build properties and possibly altering the application configuration files (such as J2EE deployment descriptors) to suit each environment.

  • The same software may need to be deployed to different application server types from different vendors. This may require editing build properties, and possibly the structure of entire targets, in order to adjust to the different tools and deployment processes required by different servers.

In general, forcing developers and application deployers to edit project buildfiles and property files on a regular basis complicates development efforts and increases the chance of human error. In general, it's preferable to have clean, modular ways to insert local configuration details into the build process so that developers, users, and deployers can simply take the project source files (in the form of a code repository checkout or a downloadable source archive), insert their local configuration details, and then use the Ant buildfile to build, test, deploy, and run the project code.

In this section, we offer some basic techniques for managing theses issues when using Ant in order to make your project buildfiles more portable and, therefore, more useful. We've packaged these techniques into a template Ant buildfile that is included in the downloadable examples for this book (see http://www.oreilly.com/catalog/javaentnut3). This template buildfile demonstrates the concepts we've discussed here and elsewhere in this chapter, and it can be used to bootstrap your own project buildfile, if needed. Make note, however, that the approaches we discuss in this section about making buildfiles portable are just some of the many approaches to answering these same issues when using Ant. Our goal here is to highlight these portability issues and to demonstrate some potentially useful solutions, not to dictate a single "correct" approach.

17.6.1. Hierarchical Property Files

One of the first issues you tend to encounter in multideveloper projects is that developers manage their environments in very different ways. Software developers typically work on code on a personal workstation, which may be running Linux, Mac OS X, Windows, or a Unix variant of some kind. These development environments have libraries and server configurations that are potentially very heterogeneous. Meanwhile, your Ant buildfile will define various properties that specify where to find dependent libraries, where relevant application server directories can be found, and so on. These properties will be defined either directly in the buildfile or in a properties file referenced by the buildfile. If these properties are defined as default values in a singular location in the project structure, the average developer will need to edit these project files to suit her local development environment. If this is a product development project involving several developers across an organization using a source code repository, the developers will need to avoid checking these buildfile changes back into the repository when they check in their code changes. When the buildfile structure changes, they will need to manually merge the new buildfile with their local changes. In the case of an open source "product" when a person downloads the source distribution to build and deploy locally, a similar manual editing process will have to be done directly on the project files and redone in the future for new releases of the open source project.

One solution to this problem is to use hierarchical property files in your Ant buildfile. In this scheme, you take advantage of the precedence rules used by Ant when defining property values, as discussed in "Properties" earlier in this chapter. Properties are set once and are invariant once they are assigned a value. So far in our example buildfiles, we've referenced a single build.properties file that contains the definition of any required properties. The hierarchical scheme involves first attempting to reference a local, user-specific properties file and then referencing the project's default properties file. Our example buildfile could be extended to do this by simply adding another <property> entry before the entry that loads the build.properties file:

 <project name="JEnt Ant portable" default="create-wars"> . . .     <!-- Import any user-specific build properties for this project -->     <property file="${user.name}-build.properties"/>     <!-- Import our default build properties -->     <property file="build.properties"/> . . . 

The first <property> enTRy looks for a properties file named <username>-build.properties, where username is the login name of the user. If the file is present, the properties it contains will be defined first, overriding any property definitions included in the default project build.properties file.

As an example of the usefulness of this scheme, our example build.properties file contains a lib.dir property that defines the base directory from which all dependent libraries are loaded, as well as several properties that specify the location of required jar files in this library directory. The project build.properties file defines default values for all of these properties:

 . . . # Directory containing libraries and other software required # to build and/or run the project code lib.dir = /usr/local/software # Libraries required by the application code xerces.home = ${lib.dir}/xerces-2_6_2 xerces.impl.jar = ${xerces.home}/xercesImpl.jar xerces.jaxp.jar = ${xerces.home}/xml-apis.jar   # JUnit libraries junit.jar = ${lib.dir}/junit-3.8.1/junit.jar . . . 

A developer might check out this project (or download the source distribution) with the intention of building the project, but they keep their class libraries in their home directory on their workstation. The developer can simply create a local properties file that overrides the lib.dir property:

 # jsmith-build.properties: Local settings for the project Ant buildfile lib.dir = ${user.home}/Software 

If the user's login name were jsmith, he would put this into a file named jsmith-build.properties in the same directory that holds the project build.properties file. Now when he invokes the Ant buildfile, the libraries will be loaded from the Software directory in the user's home directory, instead of the default location in /usr/local/software.

You can also do more fine-grained property overrides, if necessary. In our example, there might be another developer, mjones, who keeps most of the required libraries stored in the default location of /usr/local/software but keeps the Xerces libraries in another location for some reason. Then mjones can just override the xerces.home property in her local mjones-build.properties file:

 # mjones-build.properties: Local overrides xerces.home = /home/packages/xerces-2_6_2 

Various models can be applied for managing these local property files. In our case, we've assumed the property file will be named using the user's login name and will be located directly in the project directory structure. In this scheme, we can opt to include each user's property file with the project files themselves, assuming that usernames are unique among the developers working on the project. Alternatively, we could force the developers to maintain their local properties files themselves, copying them into the project directory structure when they check out or download the project source.

Another model is to reference the local properties file from a location outside of the project directory structure entirely. This prevents us from allowing developers to include their local properties files in the project structure directly but makes it easier for developers to manage their local properties files. They simply have to keep the file in the expected location in their local environment, and the project buildfile will pick it up automatically. The external location needs to be a directory that can be referenced in a portable way by the buildfilethe user's home directory is a perfect candidate since Ant has the standard user.home system property that references the user's home directory. Under this model, it doesn't make sense to include the user's login name in the local property filename since it's implied by the fact that the file is in the user's home directory. But if developers are working on multiple projects that use this same scheme for managing local properties files, they'll potentially need multiple local properties files for each project, so the filename should include the name of the project as well. To implement this model, we would need to alter our buildfile along these lines:

 <project name="ProjectX" default="create-wars"> . . .     <!-- Import any user-specific build properties for this project -->     <property file="${user.home}/${ant.project.name}-build.properties"/>     <!-- Import our default build properties -->     <property file="build.properties"/> . . . 

In this case, if a user wanted to override any of the project properties, he would create a properties file named ProjectX-build.properties in his home directory and put any local property settings there.

17.6.2. Modular Target Definitions

In the section "Enterprise Tasks," we saw several examples of common J2EE development targets that needed to be specialized depending on the application server being used. The quick-and-dirty approach we took in our earlier examples was to create multiple versions of our targets, each one suited to a different environment (e.g., we created a generic create-ejbjars target and a JBoss-specific create-ejbjars-jboss version of the same target). This approach isn't very convenient for developers, since they have to manually invoke each version of each target according to the application server they're using. It also doesn't scale well if you need to support many different environments.

One approach to dealing with this issue is to use the import task to import properties and target definitions that are specific to the server platform being used. The import task is included with Ant as of Ant 1.6 and simply allows you to import a separate buildfile into another one. The properties, targets, and other entities defined in the root project element of the imported file are included in the buildfile as if they were inserted at the point where the import task is used. Using this scheme, we import the target definitions at the top of our buildfile, like so:

 . . . <!-- Import the default server configuration --> <import file="${server.config.file}"/> . . . 

Then, our main buildfile targets can be rewritten to be general purpose, simply calling support targets defined in the imported file. Any server-specific details about the target can be embedded in the imported file, and if we want to use a different server platform, we simply replace the imported file with a different one that contains the same targets, implemented to suit the new platform.

Our create-ejbjars target, for example, can be generalized to be completely server-agnostic, as shown in Example 17-11.

Example 17-11. Portable EJB archive target
 . . . <!-- Create any ejb modules required for this application --> <target name="create-ejbjars"         description="Generates an ejb-jar archive for this application"         depends="compile">     <!-- Use the as-create-ejb-jar target to create any EJB modules          needed for this application. -->     <antcall target="as-create-ejb-jar">         <param name="as.ejb.name" value="ProfileEJB"/>         <param name="as.ejb.desc.dir" value="${config.dir}"/>         <param name="as.ejb.classes.dir" value="${java.classes.dir}"/>         <param name="as.ejb.jar.dir" value="${basedir}"/>     </antcall> </target> . . . 

All we're doing here is deferring to an as-create-ejb-jar "helper" target, which we expect to be defined in the imported file. The helper target names are prefixed by as (short for "application server") to make it easy to distinguish them from the targets defined in the main buildfile itself. When we invoke the helper target, we pass in a set of property values using child <param> elements on the antcall task. These parameters are specific to the project itself and include details such as where we want the archive created, where the EJB component classes can be found, and so on.

With our main buildfile rewritten to invoke these helper targets, we can define the targets in the imported buildfile to suit the application server being used. In the JBoss "implementation" of the helper targets, for example, we would define the as-create-ejb-jar target as shown in Example 17-12.

Example 17-12. JBoss version of the EJB archive "helper" target
 . . . <!-- Target: as-create-ejb-jar          Creates an EJB archive deployable to the target application server.  --> <target name="as-create-ejb-jar"         description="Assemble an EJB archive using the project properties">     <echo message="Generating EJB archive '${as.ejb.name}'"/>     <!-- Invoke the ejbjar task, looking for deployment descriptors          in the ${as.ejb.desc.dir} directory.  We are using "directory"-based          naming here, where the name of the directory containing the EJB          deployment descriptor is used as the basis for the ejb-jar filename. -->     <ejbjar descriptordir="${as.ejb.desc.dir}"             srcdir="${as.ejb.classes.dir}"             destdir="${as.ejb.jar.dir}"             naming="directory"             genericjarsuffix="-generic.jar"             flatdestdir="true"             dependency="super">         <!-- Specify the JBoss variant of ejbjar, to ensure that the JBoss              deployment descriptors are picked up -->         <jboss destdir="${as.ejb.jar.dir}"/>         <!-- Use local copies of descriptor DTDs provided by JBoss, in case we're              not online -->         <dtd             public             location="${jboss.home}/docs/dtd/ejb-jar_2_0.dtd"/>         <!-- Parse only EJB descriptors in files ending with "ejb-jar.xml" -->         <include name="**/*ejb-jar.xml"/>     </ejbjar> </target> . . . 

If someone wants to use our project buildfile in another server environment, all she needs to do is create a version of the import file with the required helper targets defined for that alternative platform and import the new set of target definitions into the main buildfile.

To make this approach even more portable, we can use a property to define the name of the imported file with the target definitions:

 . . . <import file="${server.config.file}"/> . . . 

This way, we can specify the default value for the filename in our build.properties file:

 server.config.file = as-config.xml 

Then, assuming we're using the hierarchical properties scheme described earlier, a user can specify an alternative set of target definitions for her local environment in her properties override file:

 server.config.file = jfarley-as-config.xml

17.6.2.1. Importing targets in older Ant versions

As mentioned at the start of this section, the import task was introduced in Ant 1.6. If you're using an earlier version of Ant, you can still implement modular target definitions, using standard XML "imports."

An Ant buildfile is an XML file, so you can include entity definitions in the header of the file, per the XML standard. These are placed inside the DOCTYPE element for the document, which is placed before any actual content in the file. The following example shows an Ant buildfile defining an entity named as-config loaded from a local file named as-config.xml and then inserting the entity in the buildfile:

 <!DOCTYPE project [     <!ENTITY as-config SYSTEM "file:./as-config.xml"> ]>   <project name=. . .> . . .     &as-config; . . . 

According to the syntax rules of XML, anything in the file referenced by the ENTITY element will be included verbatim at the point that the entity is referenced in the XML document. So we can specify our target definitions in a separate XML file and import it in a modular way into our buildfile. If we wanted to change the target definitions to suit the local environment, we could swap out our include file for the default one, or we could use multiple entities to allow users to override the default target definitions with their own:

 <!DOCTYPE project [     <!ENTITY as-config SYSTEM "file:./as-config.xml">     <!ENTITY as-user-config SYSTEM "file:./as-user-config.xml"> ]>   <project name=. . .> . . .     <!-- Include the user's target definitions first,-->     <!--so they take precedence-->     &as-user-config;     <!--Include the default target definitions -->     &as-config; . . . 

17.6.3. Putting It All Together

We've combined all of these portability schemes into a single Ant template, which you can find in the Chapter 17-Ant/template directory of the source bundle for this book (you can download it from http://www.oreilly.com/catalog/javaentnut3). The helper targets included in the import file cover the creation of web and EJB component modules and application archives and deploying applications to the application server, and we've included versions of the helper targets implemented for a few popular application servers to demonstrate the concept.

With all of these portability measures applied in a project buildfile, developers or deployers can work with the project without having to adjust the project buildfiles directly. They simply need to create a properties file with their custom set of project property values (library paths, filenames, etc.), and an import file containing target definitions tuned to their application server environment, and away they go.



Java Enterprise in a Nutshell
Java Enterprise in a Nutshell (In a Nutshell (OReilly))
ISBN: 0596101422
EAN: 2147483647
Year: 2004
Pages: 269

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net