The project I'm working on is a bit different from those I'm used to. I'm used to working on web applications that are hosted on servers and customers access with their browser. SaaS if you will. My current client is different. They're a product company that sells applications and distributes them to customers via download and CD. Their customers install these applications on internal servers (supported servers include WebSphere, WebLogic and Tomcat).
The product I'm currently working on is structured as a SOFEA application and therefore consists of two separate modules - a backend and a frontend. Since it's installed in a servlet container, both modules are WARs and can be installed separately.
Building the backend and frontend as separate projects makes a lot of sense for two reasons:
- In development, different teams can work on the frontend and backend projects.
- Having them as separate projects allows them to be versioned separately.
However, having them as two separate projects does make it a bit more difficult for distribution. I'm writing this post to show you how I recently added support for distributing our application as 2 WARs or 1 WAR using the power of Maven, war overlays and the UrlRewriteFilter.
Project Setup
First of all, we have several different Maven modules, but the most important ones are as follows:
- product-services
- product-client
- product-integration-tests
Of course, our modules aren't really named "product", but you get the point. The services project is really just a WAR project with Spring Security configured. It depends on other JAR modules that the services exist in. The client project is a GWT WAR that has a proxy servlet defined in its web.xml that makes it easier to develop. It also contains some UrlRewrite configuration that allows GWT Log's Remote Logging feature to work. The proxy servlet is something we don't want to ship with our product, so we have a separate web.xml for production vs. development. We do the substitution using the maven-war-plugin:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.0.2</version>
<configuration>
<!-- Production web.xml -->
<webXml>src/main/resources/web.xml</webXml>
<warSourceDirectory>war</warSourceDirectory>
<!-- Exclude everything but urlrewrite JAR -->
<warSourceExcludes>
WEB-INF/lib/aop**,WEB-INF/lib/commons-**,WEB-INF/lib/gin-**,
WEB-INF/lib/guice-**,WEB-INF/lib/gwt-**,WEB-INF/lib/gxt-**,
WEB-INF/lib/junit-**
</warSourceExcludes>
</configuration>
</plugin>
I could exclude WEB-INF/lib/** and WEB-INF/classes/**, but in my particular project, we still want UrlRewrite in standalone mode, and we have some i18n properties files in WEB-INF/classes that are served up for Selenium tests.
With this configuration, we have a services WAR and a client WAR that can be installed and used by clients. To collapse them into one and make it possible to ship a single war, I turned to our product-integration-tests module. This module contains Selenium tests that test both types of distributions.
Merging 2 WARs into 1
The most important thing in the product-integration-tests module is that it creates a single WAR. First of all, it uses <packaging>war</packaging>
to make this possible. The rest is done using the following 3 steps.
1. Its dependencies include the client and servlet WARs (and Selenium RC for testing).
<dependencies>
<dependency>
<groupId>com.company.app</groupId>
<artifactId>product-services</artifactId>
<version>1.0-SNAPSHOT</version>
<type>war</type>
</dependency>
<dependency>
<groupId>com.company.app</groupId>
<artifactId>product-client</artifactId>
<version>1.0-SNAPSHOT</version>
<type>war</type>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium.client-drivers</groupId>
<artifactId>selenium-java-client-driver</artifactId>
<version>1.0.1</version>
<scope>test</scope>
</dependency>
</dependencies>
2. The WAR created excludes the "integration-tests" part of the name:
<build>
<finalName>product-${project.version}</finalName>
...
</build>
3. WAR overlays are configured so the everything in the client's WEB-INF directory is excluded from the merged WAR.
<plugin>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<!-- http://maven.apache.org/plugins/maven-war-plugin/overlays.html -->
<overlays>
<overlay>
<groupId>com.company.app</groupId>
<artifactId>product-services</artifactId>
<excludes>
<!-- TODO: Rename to api.html (this is the Enunciate-generated documentation) -->
<exclude>index.html</exclude>
</excludes>
</overlay>
<!-- No server needed in product-client -->
<overlay>
<groupId>com.company.app</groupId>
<artifactId>product-client</artifactId>
<excludes>
<exclude>WEB-INF/**</exclude>
</excludes>
</overlay>
<!-- Only include META-INF/context.xml to set the ROOT path -->
<overlay>
<excludes>
<exclude>WEB-INF/**</exclude>
</excludes>
</overlay>
</overlays>
</configuration>
</plugin>
That's it! Using this configuration, it's possible to distribute a Maven-based SOFEA project as single or multiple WARs. However, there are some nuances.
One thing you might notice is the reference to META-INF/context.xml in the overlays configuration. This subtly highlights one issue I experienced when merging the WARs. In our GWT client, we're using URLs that point to our services at /product-services/*. This works in development (via a proxy servlet) and when the WARs are installed separately - as long as the services WAR is installed at /product-services. However, when they're merged, a little URL rewriting needs to happen. To do this, I added the UrlRewriteFilter to the product-services module and configured a simple rule.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCENGINE urlrewrite PUBLIC "-//tuckey.org//DTD UrlRewrite 3.0//EN"
"http://tuckey.org/res/dtds/urlrewrite3.0.dtd">
<urlrewrite use-query-string="true">
<!-- Used when services are merged into WAR with GWT client -->
<rule>
<from>^/product-services/(.*)$</from>
<to type="forward">/$1</to>
</rule>
</urlrewrite>
Because the services URLs point to the root (/product-services), the merged WAR has to be installed as the ROOT application. When you're using Cargo with Tomcat and want to deploy to ROOT, you have to have a META-INF/context.xml with a path="" reference (ref: CARGO-516).
<Context path=""/>
It is possible to change the URLs in the client to be relative, but this gets seems to get messy when you're using separate WARs. When using relative URLs, I found I had to do solution using cross-context forwarding to get the results I wanted. Using a redirect instead of a forward worked, but resulted in the client talking to the server twice (once to get redirected, a second time for the actual call). Cross-context forwarding is supported by the UrlRewriteFilter and Tomcat, but I'm not sure WebSphere or WebLogic support it. The best solution is probably to change the URLs dynamically at runtime, possibly using some sort of deferred binding technique.
Testing with Cargo and Selenium
Once I had everything merged, I wanted to configure Cargo and Selenium to allow testing both distribution types. If I installed all 3 wars at the same time, the "product-services" WAR would be used by both the product-client.war and the product.war, so I had to use profiles to allow installing the single merged WAR or both WARs. Below is the profile I used for starting Cargo, deploying the merged WAR, starting Selenium RC and running Selenium tests.
<properties>
<cargo.container>tomcat6x</cargo.container>
<cargo.container.url>
http://archive.apache.org/dist/tomcat/tomcat-6/v6.0.20/bin/apache-tomcat-6.0.20.zip
</cargo.container.url>
<cargo.host>localhost</cargo.host>
<cargo.port>23433</cargo.port>
<cargo.wait>false</cargo.wait>
<cargo.version>1.0</cargo.version>
<!-- *safari and *iexplore are additional options -->
<selenium.browser>*firefox</selenium.browser>
</properties>
...
<profile>
<id>itest-bamboo</id>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.cargo</groupId>
<artifactId>cargo-maven2-plugin</artifactId>
<version>${cargo.version}</version>
<configuration>
<wait>${cargo.wait}</wait>
<container>
<containerId>${cargo.container}</containerId>
<log>${project.build.directory}/${cargo.container}/cargo.log</log>
<zipUrlInstaller>
<url>${cargo.container.url}</url>
<installDir>${installDir}</installDir>
</zipUrlInstaller>
</container>
<configuration>
<home>${project.build.directory}/${cargo.container}/container</home>
<properties>
<cargo.hostname>${cargo.host}</cargo.hostname>
<cargo.servlet.port>${cargo.port}</cargo.servlet.port>
</properties>
<!-- Deploy as ROOT since XHR requests are made to /product-services -->
<deployables>
<deployable>
<properties>
<context>ROOT</context>
</properties>
</deployable>
</deployables>
</configuration>
</configuration>
<executions>
<execution>
<id>start-container</id>
<phase>pre-integration-test</phase>
<goals>
<goal>start</goal>
</goals>
</execution>
<execution>
<id>stop-container</id>
<phase>post-integration-test</phase>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>selenium-maven-plugin</artifactId>
<version>1.0</version>
<executions>
<execution>
<phase>pre-integration-test</phase>
<goals>
<goal>start-server</goal>
</goals>
<configuration>
<background>true</background>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<executions>
<execution>
<phase>integration-test</phase>
<goals>
<goal>test</goal>
</goals>
<configuration>
<excludes>
<exclude>none</exclude>
</excludes>
<includes>
<include>**/*SeleniumTest.java</include>
</includes>
<systemProperties>
<property>
<name>selenium.browser</name>
<value>${selenium.browser}</value>
</property>
<property>
<name>cargo.port</name>
<value>${cargo.port}</value>
</property>
</systemProperties>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
This profile is run by our Bamboo nightly tests with mvn install -Pitest-bamboo
. The 2nd profile I added doesn't install the project's WAR, but instead installs the two separate WARs. Running mvn install -Pitest-bamboo,multiple-wars
executes the Selenium tests against the multi-WAR distribution.
<profile>
<id>multiple-wars</id>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.cargo</groupId>
<artifactId>cargo-maven2-plugin</artifactId>
<version>${cargo.version}</version>
<configuration>
<configuration>
<home>${project.build.directory}/${cargo.container}/container</home>
<properties>
<cargo.hostname>${cargo.host}</cargo.hostname>
<cargo.servlet.port>${cargo.port}</cargo.servlet.port>
</properties>
<deployables>
<deployable>
<groupId>com.company.app</groupId>
<artifactId>product-client</artifactId>
<pingURL>http://${cargo.host}:${cargo.port}/product-client/index.html</pingURL>
<type>war</type>
<properties>
<context>/product-client</context>
</properties>
</deployable>
<deployable>
<groupId>com.company.app</groupId>
<artifactId>product-services</artifactId>
<pingURL>
http://${cargo.host}:${cargo.port}/project-services/index.jspx
</pingURL>
<type>war</type>
<properties>
<context>/product-services</context>
</properties>
</deployable>
</deployables>
</configuration>
</configuration>
</plugin>
</plugins>
</build>
</profile>
I won't be including any information on authoring Selenium tests because there's already many good references. I encourage you to checkout the following if you're looking for Selenium testing techniques.
Summary
This article has shown you how I used Maven, war overlays and the UrlRewriteFilter to allow create different distributions of a SOFEA application. I'm still not sure which packaging (1 WAR vs. 2) mechanism is best, but it's nice to know there's options. If you package and distribute SOFEA applications, I'd love to hear about your experience in this area.