f your software development lifecycle is anything like mine, it has several phases that each requires its own project configurationswhich can make moving a project through all the different phases a challenge. For instance, your development phase may require you to connect to a local database, but your integration test environment database won't be local. And your test database will certainly differ from your production database (for your sake, I hope it does.).
Apache Maven 2 can help. It enables you to create a single portable Project Object Model (POM), which will relieve the integration tester and the deployment team from making changes to the project. In addition to providing enforcement of project structure, project dependency management, project encapsulation, built-in site generation, and simple integration with tools such as Subversion and Continuum, Maven aims to make portability as simple as possible while maintaining flexibility. To that end, Maven has two main tools to deal with build portability issues: properties and profiles. (See "Sidebar 1. From Make to Maven" for a closer look at the history of Java-build portability.)
env.XPrefixing a variable with "env." will return the shell's environment variable. For example, ${env.PATH} contains the $path environment variable (%PATH% in Windows).project.xA dot (.) notated path in the POM will contain the corresponding element's value. For example, <project><version>1.0</version></project> is accessible via ${project.version}.java.lang.System.getProperties() are available as POM properties, such as ${java.home}. (See "Sidebar 2. Viewing Property Values" for a shortcut to debugging your POM.)xSet within a <properties /> element or an external file, the value may be used as ${someVar}.Along with properties, Maven has added the concept of build profiles as a solution for making sweeping changes to the POM based on environmental factors. The common practice in Ant-based builds is to use properties that dictate how a project will build across environments. Maven simplifies this by removing such procedural approaches with a declaration: "If some profile is active, then use these settings." You can activate profiles via:
-P <profileID><activeProfile>profileID</activeProfile> elementFor more details on profiles, I highly recommend you read the Maven site's guide.
As you may imagine, being the highest level of portability makes it generally the most difficult to attain. It restricts your dependencies to those projects and tools that may be widely distributed according to their licenses (such as most commercial software packages). It also restricts dependencies to those pieces of software that may be distributed as Maven artifacts. For example, if you depend upon MySQL, your users will have to download and install it; this is not widely portable (only MySQL users can use your project without changing their systems). Using HSQLDB, on the other hand, is available from Maven Central repository, and thus is widely portable.
The example project is environmentally portable.
See "Sidebar 4. Where to Draw the Line?" for help on deciding on which level your project should fall.
<dependency>
<groupId>com.closedsource</groupId>
<artifactId>sometool</artifactId>
<version>1</version>
<scope>system</scope>
<systemPath>/usr/lib/sometool.jar</systemPath>
</dependency>
If the system scope is unavoidable, you should use a property for systemPath. This allows you to set/alter that property per build environment. The best method, however, is to deploy the dependency to an in-house Maven repository (shown below) and use the dependency as a standard scope. (See "Sidebar 5. Grouping Absolute Values" for an addendum to the previous statement.)
<filters>
<filter>datasource.properties</filter>
</filters>
In the example project, the "datasource.properties" exists in the base build directory (${buildDir}) and contains the "name=value" pair for the "jdbc.url":
jdbc.url=jdbc:driver://localhost/myDB
When resources are filtered, the project replaces all matching property names with their corresponding values, taking the filter list into account. The resources to be filtered are defined by the "resources" build element. For example, this block says that your project has XML resources that should be filtered, and that the results will be put into the META-INF directory:
<resources>
<resource>
<filtering>true</filtering>
<directory>src/main/resources</directory>
<targetPath>META-INF</targetPath>
<includes>
<include>*.xml</include>
</includes>
</resource>
</resources>
You can run the example in the sample project by executing the "process-resources" phase in the command line. It will convert the "datasource.xml" resource file from this:
<datasource>
<jdbc-url>${jdbc.url}</jdbc-url>
</datasource>
Into this (in the "target/META-INF" directory):
<datasource>
<jdbc-url>jdbc:driver://localhost/myDB</jdbc-url>
</datasource>
-P argument on the command line. You can test which profiles are currently active with the "help" plugin, like this:
mvn help:active-profiles
You can make environmental changes even simpler by utilizing the "settings.xml" file. The example project contains three profiles: env-dev, env-test, and env-prod. If you wish to ensure that the env-test profile is always activated on your test environment, add the activeProfile to that environment's "settings.xml" file, as follows:
<settings>
...
<activeProfiles>
<activeProfile>env-test</activeProfile>
</activeProfiles>
</settings>
For every environment-specific project, name the test profile env-test. Maven will simply ignore the "activeProfile" line if it does not exist. No harm, no foul. The sample project contains a "settings.xml" file. Feel free to experiment.
<profiles>
<profile>
<id>client-0001</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<!-- The client loves blue -->
<css.pref>blue.css</css.pref>
<!-- They want standard pkg -->
<module>default</module>
</properties>
</profile>
</profiles>
Create one profile per "profiles.xml" and make it activeByDefault. Its very existence will ensure that the profile is utilized in the build. If the above "profiles.xml" file is in the base build directory, the help:active-profiles goal will print this:
The following profiles are active:
- client-0001 (source: profiles.xml)
A minor task that I often insist upon is creating environment profiles rather than passing in command-line properties, even if the profile consists of only one property. The logic behind this is simple. If you created the following profile for an environment:
<profiles>
<profile>
<id>env-test</id>
<properties>
<install.location>/testbox/app</install.location>
</properties>
</profile>
</profiles>
You command-line activation would be this:
mvn install -P env-test
instead of this:
mvn install -Dinstall.location=/user/local/test
Now imagine that you need to add another property. The manual property settings will become longer, while the profiled POM command-line remains fixed, regardless of the number of properties you may add in the future:
mvn install -Dinstall.location=/user/local/test -Dnew.prop=true
This becomes important if you decide to use a continuous integration server like Continuum.
This leads me into another nefarious piece of Maven non-portability: project nesting. For example, nesting WARs in EARs like so:
myEar
pom.xml
META-INF/application.xml
myWar
src/MyServlet.java
WEB-INF/web.xml
Before Maven, this was a common setup for projects that consisted of a WAR with a single EAR. Antenforcing no structure at allhappily obliged. Sometimes people attempt to port structures like this to Maven and build projects via embedded Ant or with the "maven-assembly-plugin". Say you write such a project for a single customer. Then, along comes a new customer who requires a different EAR configuration. Your WAR is logically a separate project, but porting it into another EAR proves quite difficult. Your ability to port this project to a new customer is now limited.
This is an example of attempting to force Maven non-standard project structures into the Maven framework. In this particular example, you should split the EAR and WAR into separate projects, like this:
myEar
pom.xml
src/main/resources/META-INF/application.xml
myWar
pom.xml
src/main
java/MyServlet.java
resources/WEB-INF/web.xml
Have a new client with a new EAR? No problem, just add a new EAR project with a dependency on myWar. Don't worry; you'll learn to love it.
<project>
<artifactId>my-parent</artifactId>
...
<build>
<extensions>
<extension>
<groupId>org.apache.maven.wagon</groupId>
<artifactId>wagon-webdav</artifactId>
<version>1.0-beta-1</version>
</extension>
</extensions>
</build>
<distributionManagement>
<repository>
<id>codehaus-mojo</id>
<name>Repository Name</name>
<url>dav:https://dav.codehaus.org/repository/mojo/</url>
</repository>
</distributionManagement>
</project>
The URL prefix corresponds to one of the supported Wagon providers, the mechanism that Maven uses to transport your files to the correct repository. (Click here for the list of supported provider types.) Besides specifying the distribution management, you must add a build extension corresponding to the transport mechanism you wish to use. In this case, WebDAV, so I added "wagon-webdav".
In order to control who may deploy their builds to this server, you will probably assign your core developers usernames. Each user can set up his or her settings.xml files (under their Maven install conf or local repository directories) to the matching repository ID, as follows:
<settings>
...
<servers>
<server>
<id>codehaus-mojo</id>
<username>joe</username>
<password>c4ntGuessThi5</password>
</server>
</servers>
...
</settings>
Requiring a change to your developer's local setup in this way does not really affect portability. You are concerned with only your client builder's ability to build and install without making local changes. That does not mean you want to make it easy for them to deploy to your repository.
Now for any project that depends upon other deployed projects, you can add the public face of your repository (made public by a simple Web server, such as Apache HTTP Server) via the repository element, like this:
<project>
...
<repositories>
<repository>
<id>codehaus-mojo</id>
<url>http://repository.codehaus.org/org/codehaus/mojo/</url>
</repository>
</repositories>
</project>
Your project is now widely portable. If you wish to be merely in-house portable, you can restrict network access, effectively creating an exclusive club for your repository (all you need is the secret handshake).
mvn deploy:deploy-file -DgroupId=com.closedsource -DartifactId=sometool -Dversion=1.0 -Dpackaging=jar
-Dfile=sometool.jar -DgeneratePom=true -Durl=scp://inhouse/maven -DlocalRepository=inhouse
Viola! Your non-portable project is now portable in-house, subject to licensing restrictions.
<repository>
<id>java.net</id>
<url>https://maven-repository.dev.java.net/nonav/repository</url>
<layout>legacy</layout>
</repository>
<repository>
<id>maven2-repository.dev.java.net</id>
<name>Java.net Repository for Maven</name>
<url>https://maven2-repository.dev.java.net/nonav/repository</url>
</repository>
Your once non-portable project using the email API is now widely portable, with no manual installs required.
| DevX is a division of Jupitermedia Corporation © Copyright 2007 Jupitermedia Corporation. All Rights Reserved. Legal Notices |