WAR Notes

Russell Bateman
January 2013
last update:


Hierarchical JARs

It's very conveniently self-documenting to subsume JAR files in a project under subdirectories named for the origin of those JARs. However, ...

Because the Java class loader doesn't search recursively all subdirectories, when you deploy a WAR file with such hierarchically arranged JARs, you will not get them: their symbols will go missing.

  $ jar -xf project.war
  $ tree WEB-INF/lib
  WEB-INF/lib
  |-- ant-contrib
  |   `-- ant-contrib-1.0b3.jar
  |-- ant-tasks
  |   `-- sqlscriptpreprocessor.jar
  |-- apache
  |   |-- commons-collections-3.1.jar
  |   |-- commons-configuration-1.7.jar
  |   |-- commons-lang-2.6.jar
  |   |-- log4j-1.2.16.jar
  |   `-- servlet-api.jar
  |-- gson
  |   `-- gson-1.7.1.jar
  |-- hibernate
  |   |-- antlr-2.7.6.jar
  |   |-- c3p0-0.9.1.jar
  |   |-- hibernate3.jar
  |   |-- hibernate-jpa-2.0-api-1.0.1.Final.jar
  |   |-- javassist-3.12.0.GA.jar
  |   |-- jta-1.1.jar
  |   |-- slf4j-api-1.6.1.jar
  |-- jboss
  |   `-- webplatform-servicelocator-1.0.jar
  |-- jersey
  |   |-- asm-3.1.jar
  |   |-- jersey-client-1.6.jar
  |   |-- jersey-core-1.6.jar
  |   |-- jersey-json-1.6.jar
  |   |-- jersey-server-1.6.jar
  |   |-- oauth-server-1.10.jar
  |   `-- oauth-signature-1.10.jar
  |-- mockito
  |   |-- mockito-all-1.8.5.jar
  |   `-- powermock-mockito-1.4.9-full.jar
  |-- mongo
  |   |-- mongo-2.6.3.jar
  |   `-- morphia-0.99.jar
  |-- mysql
  |   `-- mysql-connector-java-5.1.16-bin.jar
  |-- postgres
  |   `-- postgresql-9.1-901.jdbc4.jar
  `-- spring
      `-- org.springframework.transaction-3.0.6.RELEASE.jar

  12 directories, 30 files

No, this will never work. Instead, you must copy all of the JARs to some flat place and thence include them in the WAR. Here are some targets to put into build.xml to achieve this in bold below. The very cookie jar itself is in green.

    ...
    <property name="deployed_libraries" value="deployed-libraries" />
    ...

    <target name="clean">
        <echo message="==== Cleaning up old targets... ====" />
        ...
        <delete dir="${deployed_libraries}" />   <!-- Remove deployed_libraries subdirectory if there -->
        ...
        <echo message="==== Cleaning complete ====" />
    </target>

    <target name="init" depends="clean">
        <echo message="==== Initializing... ====" />
        ...
        <mkdir dir="${deployed_libraries}" />    <!-- Create deployed_libraries subdirectory -->
        ...
        <echo message="==== Initialization complete ====" />
    </target>

    <target name="deploy" depends="compile,updatedb">
        <echo message="==== Creating WAR file and deploying... ====" />
        <!-- Copy all the files needed to build WAR: -->
        <copy todir="${build}/classes/resources">
            <fileset dir="${src}/resources">
            <include name="**/*" />
        </fileset>
        </copy>
        <copy file="${src}/log4j.properties" todir="${build}/classes"/>
        <copy file="webplatform-servicelocator.log" todir="${build}/classes"/>

        <copy todir="${deployed_libraries}" flatten="true">
            <fileset dir="${lib}">
                <include name="**/*.jar" />
                <exclude name="**/*ant-contrib*" />    <!-- only used by build.xml -->
                <exclude name="**/*sqlscript*" />      <!-- only used by build.xml -->
                <exclude name="**/*mockito*" />        <!-- only used in testing -->
            </fileset>
        </copy>

        <war destfile="${dist}/blackpearl.war" webxml="${web}/WEB-INF/web.xml">
            <fileset dir="${web}" />
            <lib dir="${deployed_libraries}" />
            <classes dir="${build}/classes/" />
            <manifest>
                 <attribute name="Built-By"               value="Snapfish Web Platform Team" />
                 <attribute name="Implementation-Title"   value="User Account Service" />
                 <attribute name="Implementation-Version" value="0.9" />
                 <attribute name="Implementation-Vendor"  value="Hewlett-Packard Development Company, LP." />
             </manifest>
        </war>

        <copy file="${dist}/blackpearl.war" todir="${deploy}" />
        <echo message="==== Deploy complete ====" />
    </target>

Once the lib subdirectory is flattened and unused JARs are excluded, this is the result.

  $ jar -xf project.war
  $ tree WEB-INF/lib
  WEB-INF/lib
  |-- antlr-2.7.6.jar
  |-- asm-3.1.jar
  |-- c3p0-0.9.1.jar
  |-- commons-collections-3.1.jar
  |-- commons-configuration-1.7.jar
  |-- commons-lang-2.6.jar
  |-- gson-1.7.1.jar
  |-- hibernate3.jar
  |-- hibernate-jpa-2.0-api-1.0.1.Final.jar
  |-- javassist-3.12.0.GA.jar
  |-- jersey-client-1.6.jar
  |-- jersey-core-1.6.jar
  |-- jersey-json-1.6.jar
  |-- jersey-server-1.6.jar
  |-- jta-1.1.jar
  |-- log4j-1.2.16.jar
  |-- mongo-2.6.3.jar
  |-- morphia-0.99.jar
  |-- mysql-connector-java-5.1.16-bin.jar
  |-- oauth-server-1.10.jar
  |-- oauth-signature-1.10.jar
  |-- org.springframework.transaction-3.0.6.RELEASE.jar
  |-- postgresql-9.1-901.jdbc4.jar
  |-- servlet-api.jar
  |-- slf4j-api-1.6.1.jar
  `-- webplatform-servicelocator-1.0.jar

WAR manifest

Note: there is a MANIFEST.MF associated with JARs, especially executable JARs. This information, compiled specifically to deal with web-application details, may or may not apply with any accuracy.

First, do not create src/main/webapp/META-INF/MANIFEST.MF or this file will simply be copied to your WAR (with the very static content you put inside). The correct way to gain a manifest is via the maven-war-plugin.

To cause the maven-war-plugin to generate a MANIFEST.MF, you don't need to do anything, but you can force it to amplify what it adds:

<plugin>
  <artifactId>maven-war-plugin</artifactId>
  <version>3.2.2</version>
  <configuration>
    <archive>
      <manifest>
        <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
        <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
      </manifest>
    </archive>
  </configuration>
</plugin>

MANIFEST.MF information

Without further specification, the maven-war-plugin generates the following information:

  1. Manifest-Version
  2. Created-By Apache Maven + version number
  3. Build-Jdk + the Java version

The manifest generated by the maven-war-plugin contains the following bits of information derived from the noted Maven property.

Manifest property Corresponding Maven property
Manifest-Version  
Created-By: Apache Maven ${maven.version}
Build-Jdk ${java.version}
Specification-Title ${project.name}
Specification-Version ${project.artifact.selectedVersion.majorVersion} . ${project.artifact.selectedVersion.minorVersion}
Specification-Vendor ${project.organization.name}
Implementation-Title ${project.name}
Implementation-Version ${project.version}
Implementation-Vendor ${project.organization.name}

Reading MANIFEST.MF...

You might wish to do this in order to display some copyright and status information at some point in your web application, something like this:

protected static String readManifest( ServletContext servletContext )
{
  StringBuilder sb = new StringBuilder();

  try
  {
    Manifest   manifest   = new Manifest( servletContext.getResourceAsStream( MANIFEST_PATH ) );
    Attributes attributes = manifest.getMainAttributes();
    String     line;

    if( isNotEmpty( line = attributes.getValue( "Manifest-Version" ) ) )
      sb.append( "        Manifest-Version: " ).append( line ).append( '\n' );
    if( isNotEmpty( line = attributes.getValue( "Implementation-Title" ) ) )
      sb.append( "    Implementation-Title: " ).append( line ).append( '\n' );
    if( isNotEmpty( line = attributes.getValue( "Implementation-Version" ) ) )
      sb.append( "  Implementation-Version: " ).append( line ).append( '\n' );
    if( isNotEmpty( line = attributes.getValue( "Specification-Title" ) ) )
      sb.append( "     Specification-Title: " ).append( line ).append( '\n' );
    if( isNotEmpty( line = attributes.getValue( "Implementation-Vendor-Id" ) ) )
      sb.append( "Implementation-Vendor-Id: " ).append( line ).append( '\n' );
    if( isNotEmpty( line = attributes.getValue( "Build-Time" ) ) )
      sb.append( "              Build-Time: " ).append( line ).append( '\n' );
    if( isNotEmpty( line = attributes.getValue( "Created-By" ) ) )
      sb.append( "              Created-By: " ).append( line ).append( '\n' );
    if( isNotEmpty( line = attributes.getValue( "Build-Jdk" ) ) )
      sb.append( "               Build-JDK: " ).append( line ).append( '\n' );
    if( isNotEmpty( line = attributes.getValue( "Specification-Version" ) ) )
      sb.append( "   Specification-Version: " ).append( line ).append( '\n' );

    return sb.toString();
  }
  catch( IOException ex )
  {
    return "Problem reading META-INF/MANIFEST.MF";
  }
}

private static boolean isNotEmpty( final String string )
{
  return ( nonNull( string ) && string.length() > 0 );
}

Naming the WAR file in Maven

This is only a matter of configuration of maven-war-plugin. Here is a sample pom.xml. (Sorry for the mess; it's a very old, working file that I'm not going to clean up.) Highlighted are the relevant contributing lines to naming the WAR file.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>
  <groupId>com.windofkeltia.mdht</groupId>
  <artifactId>hl7v3-generator</artifactId>
  <version>3.2.0</version>
  <packaging>war</packaging>

  <properties>
    <!-- JEE and Java-relevant versions: -->
    <jersey2.version>2.27</jersey2.version>
    <jersey-bundle.version>1.19.4</jersey-bundle.version>
    <jaxrs.version>2.0.1</jaxrs.version>
    <servlet-api.version>4.0.0</servlet-api.version>
    <logback-version>1.2.3</logback-version>
    <slf4j.version>1.7.7</slf4j.version>
    <log4j.version>1.2.17</log4j.version>
    <junit.version>4.12</junit.version>
    <restassured.version>2.9.0</restassured.version>
    <maven-antrun-plugin.version>3.0.0</maven-antrun-plugin.version>
    <maven-compiler-plugin.version>3.2</maven-compiler-plugin.version>
    <maven-javadoc-plugin.version>3.2.0</maven-javadoc-plugin.version>
    <maven-war-plugin.version>3.2.0</maven-war-plugin.version>
    <maven-surefire-plugin.version>2.21.0</maven-surefire-plugin.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

    <!-- MDHT, Eclipse and other MDHT-relevant versions: -->
    <eclipse-emf.version>2.12.0.v20160420-0247</eclipse-emf.version>
    <eclipse-mdht.version>3.0.0.201802220601</eclipse-mdht.version>
    <openhealthtools.version>3.0.3.20180222</openhealthtools.version>
  </properties>

  <repositories>
    <repository>
      <!-- These are the dependencies listed as "MDHT third-party, Core, CDA, etc." -->
      <id>MDHT libraries (static)</id>
      <name>mdht</name>
      <url>file://${project.basedir}/lib</url>
    </repository>
  </repositories>

  <dependencies>
    <dependency>
      <groupId>com.sun.jersey</groupId>
      <artifactId>jersey-bundle</artifactId>
      <version>${jersey-bundle.version}</version>
    </dependency>
    <dependency>
      <groupId>javax.activation</groupId>
      <artifactId>activation</artifactId>
      <version>1.1.1</version>
    </dependency>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>${servlet-api.version}</version>
      <scope>provided</scope>   <!-- (provided by Tomcat) -->
    </dependency>
    <dependency>
      <groupId>com.sun.xml.bind</groupId>
      <artifactId>jaxb-core</artifactId>
      <version>2.3.0.1</version>
    </dependency>
    <dependency>
      <groupId>javax.xml.bind</groupId>
      <artifactId>jaxb-api</artifactId>
      <version>2.3.1</version>
    </dependency>
    <dependency>
      <groupId>com.sun.xml.bind</groupId>
      <artifactId>jaxb-impl</artifactId>
      <version>2.3.1</version>
    </dependency>

    <!-- MDHT third-party dependencies -->
    <dependency>
      <groupId>lpg.runtime.java</groupId>
      <artifactId>lpg.runtime.java</artifactId>
      <version>2.0.17.v201004271640</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.emf.ecore.xmi</groupId>
      <artifactId>org.eclipse.emf.ecore.xmi</artifactId>
      <version>${eclipse-emf.version}</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.emf.ecore</groupId>
      <artifactId>org.eclipse.emf.ecore</artifactId>
      <version>${eclipse-emf.version}</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.emf.common</groupId>
      <artifactId>org.eclipse.emf.common</artifactId>
      <version>${eclipse-emf.version}</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.ocl</groupId>
      <artifactId>org.eclipse.ocl</artifactId>
      <version>3.6.0.v20160523-1914</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.ocl.common</groupId>
      <artifactId>org.eclipse.ocl.common</artifactId>
      <version>1.4.0.v20160521-2033</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.ocl.ecore</groupId>
      <artifactId>org.eclipse.ocl.ecore</artifactId>
      <version>3.6.0.v20160523-1914</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.uml2.common</groupId>
      <artifactId>org.eclipse.uml2.common</artifactId>
      <version>2.1.0.v20170227-0935</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.uml2.types</groupId>
      <artifactId>org.eclipse.uml2.types</artifactId>
      <version>2.0.0.v20170227-0935</version>
    </dependency>

    <!-- MDHT Core dependencies -->
    <dependency>
      <groupId>org.eclipse.mdht.emf.runtime</groupId>
      <artifactId>org.eclipse.mdht.emf.runtime</artifactId>
      <version>${eclipse-mdht.version}</version>
    </dependency>

    <!-- MDHT CDA dependencies -->
    <dependency>
      <groupId>org.eclipse.mdht.uml.hl7.vocab</groupId>
      <artifactId>org.eclipse.mdht.uml.hl7.vocab</artifactId>
      <version>${eclipse-mdht.version}</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.mdht.uml.hl7.datatypes</groupId>
      <artifactId>org.eclipse.mdht.uml.hl7.datatypes</artifactId>
      <version>${eclipse-mdht.version}</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.mdht.uml.hl7.rim</groupId>
      <artifactId>org.eclipse.mdht.uml.hl7.rim</artifactId>
      <version>${eclipse-mdht.version}</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.mdht.uml.cda</groupId>
      <artifactId>org.eclipse.mdht.uml.cda</artifactId>
      <version>${eclipse-mdht.version}</version>
    </dependency>

    <!-- MDHT CDA Implementation Guide dependencies -->
    <dependency>
      <groupId>org.openhealthtools.mdht.uml.cda.consol2</groupId>
      <artifactId>org.openhealthtools.mdht.uml.cda.consol2</artifactId>
      <version>${openhealthtools.version}</version>
    </dependency>

    <!-- HL7 CDA dependencies -->
    <dependency>
      <groupId>org.hl7.cbcc.privacy.consentdirective</groupId>
      <artifactId>org.hl7.cbcc.privacy.consentdirective</artifactId>
      <version>1.0.0.20170920</version>
    </dependency>
    <dependency>
      <groupId>org.hl7.security.ds4p.contentprofile</groupId>
      <artifactId>org.hl7.security.ds4p.contentprofile</artifactId>
      <version>3.0.0.20170920</version>
    </dependency>

    <!-- Various other dependencies -->
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>${logback-version}</version>
      <exclusions>
        <exclusion>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-log4j12</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <!-- provide HTTP-access log functionality -->
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-access</artifactId>
      <version>${logback-version}</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>${slf4j.version}</version>
    </dependency>
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>${log4j.version}</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>${junit.version}</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <finalName>mdht-restlet</finalName>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
        </configuration>
        <version>${maven-compiler-plugin.version}</version>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-war-plugin</artifactId>
        <version>${maven-war-plugin.version}</version>
        <configuration>
          <warName>${project.artifactId}-${project.version}</warName>
          <webResources>
            <resource>
              <!-- this is relative to the pom.xml directory -->
              <directory>src/main/webapp</directory>
            </resource>
          </webResources>

          <!-- Magic for maintaining a build timestamp in MANIFEST.MF: -->
          <archive>
            <manifest>
              <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
              <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
            </manifest>
            <manifestEntries>
              <Build-Time>${maven.build.timestamp}</Build-Time>
            </manifestEntries>
          </archive>

        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-antrun-plugin</artifactId>
        <version>${maven-antrun-plugin.version}</version>
        <executions>
          <execution>
            <phase>compile</phase>
            <configuration>
              <target>
                <!-- How to echo out properties and their definitions; uncomment this!
                <echoproperties /> -->
              </target>
            </configuration>
            <goals>
              <goal>run</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>${maven-surefire-plugin.version}</version>
        <configuration>
          <systemPropertyVariables>
            <catalina.home>${project.build.directory}</catalina.home>
            <buildDirectory>${project.build.directory}</buildDirectory>
          </systemPropertyVariables>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-javadoc-plugin</artifactId>
        <version>${maven-javadoc-plugin.version}</version>
        <configuration>
          <noqualifier>all</noqualifier>
          <destDir>javadoc</destDir>
          <show>private</show>
        </configuration>
        <executions>
          <execution>
            <id>aggregate</id>
            <goals>
              <goal>aggregate</goal>
            </goals>
            <configuration>
              <reportOutputDirectory>${user.dir}</reportOutputDirectory>
              <destDir>javadoc</destDir>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

Adding version to the WAR name has consequences...

This results (against Tomcat, at least) in needing to use the version also in the URL:

http://hostname:port/hl7v3-generator-3.2.0

This is not handy because (HTTP-client) code will have to be updated to reflect this. Instead, Tomcat Parallel deployment suggests this naming scheme:


<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-war-plugin</artifactId>
  <version>${maven-war-plugin.version}</version>
  <configuration>
    <warName>${project.artifactId}##${project.version}</warName>
    <webResources>
      <resource>
        <!-- this is relative to the pom.xml directory -->
        <directory>src/main/webapp</directory>
      </resource>
    </webResources>
    ...

...which results in a name like hl7v4-generator##3.2.0. This is ugly, but it still accomplishes the same goal of advertising the version. And, for the URL, everything past and also the ## will not be required.

Tomcat's deployment will respect the basic name (hl7v3-generator). Also, multiple deployments will accumulate in /opt/tomcat/webapps and you must delete them, but, if you do not delete them or you delete them slowly or cautiously, you will have the older versions to go back to. That might be a good thing. Meanwhile, when you bump the version and deploy, your requests will go to the latest (there are some caveats—read the link above).