Wednesday, September 5, 2007

Maven 2 and WebSphere - automated build and deployment of J2EE applications

After long search on different solutions on automated building and deployment of J2EE applications with maven that can work for WebSphere few posts were found. Some of them (big thanks to Peter Pilgrim) cover ways to build EJB modules that could be 'understood' by websphere (that is build websphere specific stubs and skeletons). Some posts show how to configure maven (including maven-eclipse-plugin) so that maven could work with RAD-6 and vice versa.
All we have to do is put it all together, automate deployment of EAR (remember cargo plugin doesn't support websphere yet) to websphere and hook functional tests (using selenium) into the integration test phase.
So the project consists of an ejb, war, ear, functional testing and a parent modules, like this:


 parent_project
|-ejb_module
|
|-war_module
|
|-ear_module
|
|-test_module




Looks like a complete set? Let's go through all the modules.





The EJB module.

The general problem is as it is well known the WebSphere requires generated stubs and skeletons packaged in ejb jar before the deployment which maven-ejb-plugin can't do. But IBM does offer java api to generate them via ant tasks. All we need to do is to 'silently' generate the server-specific files with ant tasks and repackage the ejb jar as a part of maven build cycle (usually generated by RAD during deployment in the IDE). In short this is how the pom may look like:

<project>
<modelVersion>4.0.0</modelVersion>
<groupId>your_group</ins></groupId>
<artifactId>ejb_module</artifactId>
<packaging>ejb</packaging>
<name>ejb_module</name>
<parent>
<groupId>your_group</groupId>
<artifactId>parent_project</artifactId>
<version>0.1-SNAPSHOT</version>
</parent>

<dependencies>
...
Your dependencies
...
<dependency>
<!-- provided by the container -->
<groupId>websphere</groupId>
<artifactId>j2ee</artifactId>
<version>1.4</version>
<scope>provided</scope>
</dependency>
<dependency>
<!-- websphere specific dependency, available in own repository -->
<groupId>websphere</groupId>
<artifactId>ivjejb35</artifactId>
<version>6</version>
<scope>provided</scope>
</dependency>
<dependency>
<!-- websphere specific dependency, available in own repository -->
<groupId>websphere</groupId>
<artifactId>webservices</artifactId>
<version>6</version>
<scope>provided</scope>
</dependency>
<dependency>
<!-- websphere specific dependency, available in own repository -->
<groupId>websphere</groupId>
<artifactId>webservices</artifactId>
<version>6</version>
<scope>provided</scope>
</dependency>
<dependency>
<!-- websphere specific dependency, available in own repository -->
<groupId>websphere</groupId>
<artifactId>ejbcontainer</artifactId>
<version>6</version>
<scope>provided</scope>
</dependency>
<dependency>
<!-- websphere specific dependency, available in own repository -->
<groupId>websphere</groupId>
<artifactId>ejbcontainerImpl</artifactId>
<version>6</version>
<scope>provided</scope>
</dependency>
<dependency>
<!-- websphere specific dependency, available in own repository -->
<groupId>websphere</groupId>
<artifactId>ecutils</artifactId>
<version>6</version>
<scope>provided</scope>
</dependency>
<dependency>
<!-- websphere specific dependency, available in own repository -->
<groupId>websphere</groupId>
<artifactId>ras</artifactId>
<version>6</version>
<scope>provided</scope>
</dependency>
<dependency>
<!-- websphere specific dependency, available in own repository -->
<groupId>websphere</groupId>
<artifactId>ejbportable</artifactId>
<version>6</version>
<scope>provided</scope>
</dependency>
<dependency>
<!-- websphere specific dependency, available in own repository -->
<groupId>websphere</groupId>
<artifactId>utils</artifactId>
<version>6</version>
</dependency>
</dependencies>

<build>

<finalName>${project.artifactId}</finalName>

<!-- RAD-default source tree -->
<sourceDirectory>ejbModule</sourceDirectory>
<resources>
<resource>
<directory>ejbModule</directory>
<excludes>
<exclude>**/*.java</exclude>
<exclude>**/CVS/**</exclude>
</excludes>
</resource>
</resources>


<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-ejb-plugin</artifactId>
<configuration>
<ejbVersion>2.1</ejbVersion>
<generateClient>true</generateClient>
<clientExcludes>
<clientExclude>**/ejbserver/*EJB.class</clientExclude>
</clientExcludes>
<archive>
<manifest>
<!-- generate manifest file properly -->
<addClasspath>true</addClasspath>
</manifest>
</archive>
</configuration>
</plugin>

<plugin>
<!-- Configuration of RAD specific JRE containers -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-eclipse-plugin</artifactId>
<configuration>
<classpathContainers>
<!-- org.eclipse.jdt.launching.JRE_CONTAINER is included by default, add a j2ee container here -->
<classpathContainer>com.ibm.wtp.server.java.core.container/com.ibm.ws.ast.st.runtime.core.runtimeTarget.v60/was.base.v6</classpathContainer>
</classpathContainers>
</configuration>
</plugin>

<!-- Maven Ant run plugin to run ejb-deploy automatically to simulate websphere's generation of stubs and skeletons -->
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<phase>verify</phase>
<configuration>
<tasks>
<echo>was6.home: ${was6.home}</echo>

<!-- Default path to use for WAS test version (inside RAD), override this with -D parameter if needed -->
<property name="was6.home"
value="c:/Program Files/IBM/Rational/SDP/6.0/runtimes/base_v6" />

<ant antfile="ejbdeploy.xml" inheritRefs="true" inheritAll="true">
<property name="project.name" value="${project.name}"/>
<property name="project.artifactId" value="${project.artifactId}"/>
<property name="project.groupId" value="${project.groupId}"/>
<property name="project.version" value="${project.version}"/>
<property name="project.packaging" value="${project.packaging}"/>
<property name="project.description" value="${project.description}"/>
<property name="project.parent.name" value="${project.parent.name}"/>
<property name="project.parent.artifactId" value="${project.parent.artifactId}"/>
<property name="project.parent.groupId" value="${project.parent.groupId}"/>
<property name="project.parent.version" value="${project.parent.version}"/>
<property name="project.build.directory" value="${project.build.directory}"/>

<!--
This is to enable to override the was6.home by providing -Dwas6.home=<WAS home dir>.
Use this to override the default path for the development environment (such as if you use standalone server
and it has a different location than in the called ant file)
-->

<property name="was6.home" value="${was6.home}"/>
</ant>
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>

</build>

</project>


This pom is self explanatory for the most part (and I've tried to put some comments there as well). Text in bold is to be noticed, I will just stop on most important parts:


  • The header (down to dependencies) is pretty much clear I think, no magic here...


  • Dependencies. The dependencies I've put there is what I had to find out to resolve some vendor specific dependencies (which the project unfortunately already had). If your project already happened to have some WebSphere-specific dependencies that you would most likely need to define them too... I would recommend setting up an enterprise repository (for example Artifactory) as you won't find most of those those jar's on open source repositories (those jars can be found at websphere location, mostly under lib directory). Another recommendation is to start from no websphere dependencies in the list and start adding them only if you start having compile errors due to classes not being resolved. The reason behind adding those besides specifying the classpathContainer is that classpathContainer doesn't help when you compile under maven but only adds a classpath container entry in your .classpath file for compiling in RAD (we use RAD 6).


  • The next one is <finalName>${project.artifactId}</finalName>. The reason behind this is because module names in the vendor-specific deployment descriptors are based on RAD project names which means that having your project names having 0.1-SNAPSHOT (or whatever version you have) appended at the end is not very handy (you wont be able to release that easily). So we override the artifact name instead...

  • maven-ejb-plugin is there mostly to generate ejb jar manifest file properly (and to filter stuff for ejb the generated client).

  • maven-eclipse-plugin is configured to generate J2EE container in the .classpath which is needed for hot-deployment and compiling J2EE projects in RAD (for compiling in maven we have dependencies as described above).

  • And finally maven-antrun-plugin which is configured to kick in at verify phase which executes right before install phase. The maven-antrun-plugin is capable of executing ant scripts from maven at a specified time. To keep things a bit more clean we just call an external ant file from here







The WEB module.



This one is much easier, although with RAD-6 there are some 'tips&tricks'.


<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>your_group</groupId>
<artifactId>web_module</artifactId>
<packaging>war</packaging>
<name>web_module</name>

<parent>
<groupId>your_group</groupId>
<artifactId>parent_project</artifactId>
<version>0.1-SNAPSHOT</version>
</parent>

<dependencies>
...
Your dependencies
...
</dependencies>

<build>
<finalName>${project.artifactId}</finalName>

<!-- RAD-default source tree -->
<sourceDirectory>${basedir}/JavaSource</sourceDirectory>

<!--
customizng resources location so that only needed files are archived in jar files
-->
<resources>
<resource>
<directory>${basedir}/JavaSource</directory>
<excludes>
<exclude>**/*.java</exclude>
<exclude>**/*.class</exclude>
</excludes>
</resource>
</resources>

<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<warSourceDirectory>${basedir}/src/main/webapp</warSourceDirectory>
</configuration>
</plugin>
<!--
customizing classes folder for RAD to pick it classes for hot deployment of exploded archive up - doesn't work with the default one (target)
-->

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-eclipse-plugin</artifactId>
<configuration>
<buildOutputDirectory>${basedir}/src/main/webapp/WEB-INF/classes</buildOutputDirectory>
<!-- Customizing context root if needed -->
<warContextRoot>your_context_root</warContextRoot>
<additionalBuildcommands>
<!-- Needed by RAD in a typical web project -->
<buildcommand>com.ibm.etools.ctc.serviceprojectbuilder</buildcommand>

</additionalBuildcommands>
<!-- Configuration of RAD specific JRE containers -->
<classpathContainers>
<classpathContainer>com.ibm.wtp.server.java.core.container/com.ibm.ws.ast.st.runtime.core.runtimeTarget.v60/was.base.v6</classpathContainer>
</classpathContainers>
</configuration>
</plugin>
</plugins>
</build>

</project>



Let's go through it piece-by-piece... The beginning is pretty much standard for any web module. Don't forget to specify dependencies that are provided by the container in the same way as it is done in the ejb module, also don't forget to use excludes (and there are lots of exclusions for websphere!) or disable transitive dependencies alltogether.
The next interesting part is:
<finalName>${project.artifactId}</finalName>

Which is needed in order to keep consistency between the maven-generated artifacts and RAD-generated vendor specific deployment desciptors.

The following piece:
<sourceDirectory>${basedir}/JavaSource</sourceDirectory>

is there to (still) allow to use default RAD source tree (however if you can afford moving whole source tree in your project or creating it from scratch I'd advice to use the maven default settings and not to use this one).

If we 'fall' for the above we would probably want to use the resource folder in 'RAD' way (that source with the resource files together):

<!--
customizing resources location so that only needed files are archived in jar files
-->

<resources>
<resource>
<directory>${basedir}/JavaSource</directory>
<excludes>
<exclude>**/*.java</exclude>
<exclude>**/*.class</exclude>

</excludes>
</resource>
</resources>

Source and compiled classes are excluded so they don't get in the resulting .war file.


For the current version of maven (2.0.7 at the time of writing) the webapp root dir should match the default one of maven (${basedir}/src/main/webapp), unfortunately I did not manage to make customized directory work with maven and RAD at the same time. Which could be (in theory) done by the following configuration:

            <plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<warSourceDirectory>${basedir}/src/main/webapp</warSourceDirectory>
</configuration>
</plugin>


Keep in mind it needs to be in 'sync' with <buildOutputDirectory>...</buildOutputDirectory> (or better yet - make it a property in order not to repeat yourself - DRY). And here we go:


<!--
customizing classes folder for RAD to pick it classes for hot deployment of exploded archive up - doesn't work with the default one (target)
-->

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-eclipse-plugin</artifactId>
<configuration>
<buildOutputDirectory>${basedir}/src/main/webapp/WEB-INF/classes</buildOutputDirectory>
<!-- Customizing context root if needed -->
<warContextRoot>your_context_root</warContextRoot>
<additionalBuildcommands>
<!-- Needed by RAD in a typical web project -->
<buildcommand>com.ibm.etools.ctc.serviceprojectbuilder</buildcommand>

</additionalBuildcommands>
<!-- Configuration of RAD specific JRE containers -->
<classpathContainers>
<classpathContainer>com.ibm.wtp.server.java.core.container/com.ibm.ws.ast.st.runtime.core.runtimeTarget.v60/was.base.v6</classpathContainer>
</classpathContainers>
</configuration>
</plugin>


Here, buildOutputDirectory points at the same tree branch as the warSourceDirectory.
warContextRoot is here just in case you want to customize the context root. com.ibm.etools.ctc.serviceprojectbuilder in additionalBuildcommands is needed because RAD usually generates it in .project file for eclipse for web-enabled projects. And of course the <classpathContainer/> which generates a reference required libraries for work in RAD (although you wouldn't need it if you use the enterprise repository with the vendor specific dependencies - just like in case with the ejb module).

This was the WEB project.



The EAR module.



There is really nothing special about it except this little configuration:



<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-ear-plugin</artifactId>
<configuration>
<modules> <!-- Configuring names of artifacts in EAR -->
<webModule>
<groupId>${project.groupId}</groupId>
<artifactId>web_module</artifactId>
<contextRoot>/your_context_root</contextRoot>
<bundleFileName>web_module.war</bundleFileName>
</webModule>
<ejbModule>
<groupId>${project.groupId}</groupId>
<artifactId>ejb_module</artifactId>
<bundleFileName>ejb_module.jar</bundleFileName>
</ejbModule>
</modules>
<earSourceDirectory>${basedir}</earSourceDirectory>
<earSourceIncludes>
**/ibmconfig/**
</earSourceIncludes>
<earSourceExcludes>**/target/**</earSourceExcludes>
</configuration>
</plugin>
</plugins>
</build>



Here we have configuration of the application.xml where we have hardcoded (not nice but have to...) names of modules and the resulting artifacts that are packaged in the resulting ear (remember we have to have the same artifact names as the ones hardcoded in the IBM-specific deployment descriptors).
And then there is some extra configuration telling to include the vendor specific deployment descriptors (we would need to create them in RAD) and extra configuration not to include the target directory in the resulting EAR.



Functional tests module



Functional test module (a separate maven module) is responsible for automated deployment to websphere and running the functional tests on the deployed application by using selenium).

In general, enabling fully automated integration tests for a j2ee application under websphere in this case consists of two parts: enabling selenium (a lot of info on this can be found here) and automated deployment of the application EAR to WAS before the selenium tests kick-in. Because we don't have a cargo plug-in for WAS-6 ready yet we use the ant scripts from websphere and plug them into the maven build lifecycle.

In short, running selenium tests and automated deployment of the application EAR can be done in the following steps:

  1. Define selenium repository (or have your enterprise repository take care of that)

  2. Define dependencies to be able to run selenium

  3. Automatically start selenium server just before the integration-test phase

  4. Force Surefire plugin to run in integration-test phase and not in test phase (this is a dedicated integration test module and doesn't need to run the test phase).

  5. Prepare EAR for the deployment script

  6. Call websphere deployment scripts in pre-integration test phase to deploy the EAR we prepared

  7. Run the tests (Surefire plug-in)

  8. Call websphere deployment scripts in post-integration test phase to undeploy the EAR (cleanup)



Let's go through all the steps:

  1. Define selenium repository (or have your enterprise repository take care of that). We can define it either in our pom (see below) or let artifactory take care of that:


    <repositories>
    <repository>
    <id>openqa</id>
    <name>OpenQA Repository</name>
    <url>http://maven.openqa.org</url>
    <layout>default</layout>
    <snapshots>
    <enabled>false</enabled>
    </snapshots>
    <releases>
    <enabled>true</enabled>
    </releases>
    </repository>
    </repositories>


  2. Define dependencies to be able to run selenium. This is also pretty straight forward:

    <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>3.8.1</version>
    <scope>test</scope>
    </dependency>
    <dependency>
    <groupId>org.openqa.selenium.client-drivers</groupId>
    <artifactId>selenium-java-client-driver</artifactId>
    <version>0.9.0</version>
    </dependency>
    <dependency>
    <groupId>org.openqa.selenium.server</groupId>
    <artifactId>selenium-server</artifactId>
    <version>0.9.0</version>
    </dependency>


    Here we have defined dependencies on JUnit in order to be run the selenium tests converted to JUnit, then we have client drivers to use the selenium client api in our JUnit tests and we have dependency on server which runs the selenium server to execute the tests.



  3. Automatically start selenium server just before the integration-test phase. This is defined in build/plugins section:


    <!-- Start the Selenium server -->
    <plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>selenium-maven-plugin</artifactId>
    <executions>
    <execution>
    <phase>pre-integration-test</phase>
    <goals>
    <goal>start-server</goal>
    </goals>
    <configuration>
    <background>true</background>
    <logOutput>true</logOutput>
    <multiWindow>true</multiWindow>
    <debug>true</debug>
    </configuration>
    </execution>
    </executions>
    </plugin>


    Notice that it starts in pre-integration phase, that is just before the integration phase starts.


  4. Force Surefire plugin to run in integration-test phase and not in test phase. This belongs to build/plugins section as well:


    <!--
    Forcing test phase in integration-test phase
    -->
    <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
    <!-- Skip the normal tests, we'll run them in the integration-test phase -->
    <skip>true</skip>
    </configuration>
    <executions>
    <execution>
    <phase>integration-test</phase>
    <goals>
    <goal>test</goal>
    </goals>
    <configuration>
    <skip>false</skip>
    </configuration>
    </execution>
    </executions>
    </plugin>


    Notice the part with <skip>true</skip> - it tells maven to skip the test phase for this plugin otherwise test will fail because we haven't deployed the application yet. And then for the integration phase we enforce this plugin (also notice the part with <skip>false</skip> there)


  5. Prepare EAR for the deployment script. This is an interesting one... Because we don't have cargo plugin for WAS-6 that could seamlessly deploy the EAR to the server we need to prepare the EAR at a specific location so that 3rd party scripts could pickup the archive for deployment. This is how this can be done (also in the same build/plugins section):


    <plugin>
    <!--
    This goal allows us to get the EAR from the repository for deployment and install it locally
    -->
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-dependency-plugin</artifactId>
    <executions>
    <execution>
    <id>copy</id>
    <phase>package</phase>
    <goals>
    <goal>copy</goal>
    </goals>
    <configuration>
    <artifactItems>
    <artifactItem>
    <!-- same group -->
    <groupId>${project.groupId}</groupId>
    <artifactId>your_artifact_name</artifactId>
    <type>ear</type>
    <overWrite>true</overWrite>
    <destFileName>your_artifact_name.ear</destFileName>
    </artifactItem>
    </artifactItems>
    <outputDirectory>${project.build.directory}</outputDirectory>
    <overWriteReleases>true</overWriteReleases>
    <overWriteSnapshots>true</overWriteSnapshots>
    </configuration>
    </execution>
    </executions>
    </plugin>


    The preparation of EAR for deployment is done by forcing the maven-dependency-plugin to copy resources (our EAR in this case) from local repository in package phase. Because the copying is done from the local repository we would need to use at least install (or later) phase in our parent module which would guarantee placement of our EAR in a local repository. The plugin takes the artifactId and type to identify the resource to copy, we also use the destFileName and outputDirectory to configure the final name and the location of the EAR. As a result of this configuration we get the archive we want to deploy in the target directory of this module.


  6. Call websphere deployment scripts in pre-integration test phase to deploy the EAR we prepared above to our websphere server (this can be both the websphere test environment integrated with the IDE or a dedicated server). Because we can't use cargo plugin we have to do something else - in this case we can use deployment scripts from websphere to deploy our artifacts. We configure both deploy and undeploy operations here. This is done with the help of maven antrun plugin by calling our ant scripts (they'll follow later):



    <!-- Maven Ant run plugin to run deploy the EAR automatically to WebSphere-->
    <plugin>
    <artifactId>maven-antrun-plugin</artifactId>
    <executions>
    <!--
    Deployment to the server
    -->
    <execution>
    <id>deployment</id>
    <phase>pre-integration-test</phase>
    <configuration>
    <tasks>

    <!-- default path to use for WAS test version (inside RAD), override this with -D parameter if needed -->
    <property name="was6.home"
    value="c:/Program Files/IBM/Rational/SDP/6.0/runtimes/base_v6" />
    <ant antfile="eardeploy.xml" inheritRefs="true" inheritAll="true">
    <property name="project.name" value="${project.name}"/>
    <property name="project.artifactId" value="${final.artifact.name}"/>
    <property name="project.groupId" value="${project.groupId}"/>
    <property name="project.version" value="${project.version}"/>
    <property name="project.packaging" value="${project.packaging}"/>
    <property name="project.description" value="${project.description}"/>
    <property name="project.parent.name" value="${project.parent.name}"/>
    <property name="project.parent.artifactId" value="${project.parent.artifactId}"/>
    <property name="project.parent.groupId" value="${project.parent.groupId}"/>
    <property name="project.parent.version" value="${project.parent.version}"/>
    <property name="project.build.directory" value="${project.build.directory}"/>

    <!--
    This is to enable to override the was6.home by providing -Dwas6.home=<WAS home dir>.
    Use this to override the default path for the development environment (such as if you use standalone server
    and it has a different location than in the called ant file)
    -->

    <property name="was6.home" value="${was6.home}"/>
    <!--
    Override the user.install.root by providing -Duser.install.root=<WAS profile dir>.
    Use this to override the default path for the development environment (such as if you use standalone server
    and it has a different location than in the called ant file)
    -->
    <property name="user.install.root" value="${user.install.root}"/>
    </ant>
    </tasks>
    </configuration>
    <goals>
    <goal>run</goal>
    </goals>
    </execution>
    <!--
    Undeploy from the server after the tests are done
    -->
    <execution>
    <id>undeployment</id>
    <phase>post-integration-test</phase>
    <configuration>
    <tasks>
    <ant antfile="eardeploy.xml" target="undeploy" inheritRefs="true" inheritAll="true">
    <property name="project.name" value="${project.name}"/>
    <property name="project.artifactId" value="${final.artifact.name}"/>
    <property name="project.groupId" value="${project.groupId}"/>
    <property name="project.version" value="${project.version}"/>
    <property name="project.packaging" value="${project.packaging}"/>
    <property name="project.description" value="${project.description}"/>
    <property name="project.parent.name" value="${project.parent.name}"/>
    <property name="project.parent.artifactId" value="${project.parent.artifactId}"/>
    <property name="project.parent.groupId" value="${project.parent.groupId}"/>
    <property name="project.parent.version" value="${project.parent.version}"/>
    <property name="project.build.directory" value="${project.build.directory}"/>
    </ant>
    </tasks>
    </configuration>
    <goals>
    <goal>run</goal>
    </goals>
    </execution>
    </executions>
    </plugin>


    The deployment to the server is done in the pre-integration-test phase. Because maven runs goals in order that plugins are configured in build/plugins section we have the EAR file prepared for deployment properly.
    In the configuration section of antrun plugin we defined was6.homewhich points to default location of WAS for development environment but this can be easily overridden by using -Dwas6.home=<your WAS home> in the command line.
    We could put ant calls right here in the same place but it's better to separate ant from maven to get a 'cleaner' solution.
    Undeployment is performed by specifying second execution section and calling script that performs undeploy in post-integration-test phase which happens after the integration-test phase where our tests run. In this case it is the same script eardeploy.xml only a different target (undeploy). You can also notice we define properties such as project.artifactId inside the antcall target because antcall doesn't automatically passes all variables to the ant script from withiin the maven runtime environment (so we specify them explicitly).




    And of course the ant script that performs the deployment and undeployment. It's quite big but fortunately it's not dependent on file system and thus can be easily reused across projects 'as is':


    <?xml version="1.0"?>
    <project name="was-deployment" default="deploy" basedir=".">

    <property file="deploy.properties" />

    <path id="was.classpath">
    <fileset dir="${was6.home}/lib">
    <include name="*.jar" />
    <include name="wsanttasks.jar" />
    <include name="webservices.jar" />
    <include name="wsprofile.jar" />
    <include name="j2ee.jar" />
    <include name="ffdc.jar" />
    <include name="wsdl4j.jar" />
    <include name="bootstrap.jar" />
    <include name="commons-logging-api.jar" />
    <include name="commons-discovery.jar" />
    <include name="ras.jar" />
    <include name="wsexception.jar" />
    <include name="emf.jar" />
    <include name="classloader.jar" />
    </fileset>
    <fileset dir="${was6.home}/java/jre/lib">
    <include name="xml.jar" />
    <include name="ibmorb.jar" />
    <include name="ibmorbapi.jar" />
    </fileset>
    </path>


    <target name="init-tasks">
    <taskdef name="wsInstallApp" classname="com.ibm.websphere.ant.tasks.InstallApplication">
    </taskdef>
    <taskdef name="wsUninstallApp" classname="com.ibm.websphere.ant.tasks.UninstallApplication">
    </taskdef>
    <taskdef name="wsStartApp" classname="com.ibm.websphere.ant.tasks.StartApplication">
    </taskdef>
    </target>

    <target name="ws-exec">
    <exec executable="${user.install.root}/bin/ws_ant.bat" failonerror="true">
    <arg value="-f"/>
    <arg value="eardeploy.xml"/> <!-- this is this own build file name but to be restarted with IBM implementation of ant -->
    <arg value="-Dwas6.home=${was6.home}"/>
    <arg value="${wasTarget}"/>
    <arg value="-Dear.path=${ear.path}"/>
    <arg value="-Dproject.artifactId=${project.artifactId}"/>
    </exec>
    </target>


    <target name="deploy" description="Deploys EARs to the WAS">
    <echo>
    FYI

    project.name=${project.name}
    project.artifactId=${project.artifactId}
    project.groupId=${project.groupId}
    project.version=${project.version}
    project.packaging=${project.packaging}
    project.description=${project.description}

    project.parent.name=${project.parent.name}
    project.parent.artifactId=${project.parent.artifactId}
    project.parent.groupId=${project.parent.groupId}
    project.parent.version=${project.parent.version}

    ear.path=${ear.path}
    was6.home=${was6.home}
    </echo>

    <antcall target="ws-exec">
    <param name="wasTarget" value="deploy-ear" />
    </antcall>

    </target>

    <target name="undeploy" description="Undeploys the application fromthe server">
    <antcall target="ws-exec">
    <param name="wasTarget" value="undeploy-ear" />
    </antcall>
    </target>


    <target name="deploy-ear" depends="init-tasks, undeploy-ear">
    <echo>Deploying ear ${ear.path} to websphere via ws_ant</echo>
    <echo>Host: ${host}, port: ${port}</echo>

    <wsInstallApp wasHome="${was6.home}"
    ear="${ear.path}"
    options="-usedefaultbindings -verbose true"
    conntype="SOAP"
    host="${host}"
    port="${port}"
    />

    <antcall target="start-app" />

    </target>

    <target name="start-app">
    <wsStartApp wasHome="${was6.home}"
    application="${project.artifactId}"
    conntype="SOAP"
    host="${host}"
    port="${port}"/>
    </target>

    <target name="undeploy-ear" depends="init-tasks" >
    <wsUninstallApp wasHome="${was6.home}"
    application="${project.artifactId}"
    conntype="SOAP"
    host="${host}"
    port="${port}"/>
    </target>
    </project>


    This ant script uses ant tasks wsInstallApp, wsUninstallApp and wsStartApp provided by websphere. They are all needed to deploy (wsInstallApp target) the application and start it wsStartApp (separate action in webpshere) and then to undeploy the application (wsUninstallApp target). To protect this script from often changes we have was.classpath which provides classpath for those targets and deploy.properties file provides properties that you're likely to customize per application (or environment).
    Then we have a 'strange' ws-exec target which basically runs the same script again but this time passes it through ${user.install.root}/bin/ws_ant.bat which is an own implementation of ant from IBM. We do this mostly because those ant tasks we described require a lot of system and environment variables setup. And since this is a proprietary solution of IBM it's easier to let those scripts to do the initialization job and then return to our deploy.xml. We also pass the required properties (as command line arguments) through this script which in this case are:

    * eardeploy.xml - the same script to run (in command line this is usually "-f <script name>")
    * -Dwas6.home=${was6.home} - passing the location of websphere home dir location as system variable (as you can see it's used all across the script)
    * ${wasTarget} - the next ant target in this script to run
    * -Dear.path=${ear.path} - path of the ear file for deployment
    * -Dproject.artifactId=${project.artifactId} - name of the application for undeployment (I recommend to configure EAR name as artifact name with '.ear' extension appended - for example artifactId.ear, you can also see across the scripts that we have final artifact name defined which also serves this purpose)


    The rest is pretty straight forward, we have deploy target which calls IBM version of ant implementation which in turn calls deploy-ear which actually calls the corresponding ant task. The same is valid for undeploy target. With the exception that deploy-ear target includes call to start-app target which starts the application as soon as it is deployed (those operations are synchronous). In short for deployment we have the following workflow:

     deploy target --> ws_ant (WAS environment initialization) --> deploy-ear --> start_ear


    and for undeployment:

     undeploy target --> ws_ant (WAS environment initialization) --> undeploy-ear



    Another thing to notice is that deploy also includes call for undeployment - this is provided in case if integration tests failed (which cause all later maven phases including undeployment being aborted) so that next deployment operation cleans up before its own execution. In case if the application is not deployed the undeployment fails silently without aborting the whole workflow (unfortunately if deployment fails its also silent but then our integration test also will fail because there is no application to test :) ).

    And finally we have deploy-ear, start-app and undeploy-ear which call websphere tasks directly. We also have host and port configured in deploy.properties file:


    # server host name for deployment
    host=localhost
    # server port for deployment
    port=8880





One last side-note, if you want to have a source directory in generated RAD project when you run mvn eclipse:rad you woudl want to use packaging type jar (<packaging>jar</packaging>)




Parent module



This is the last module which serves as an orchestration assembly module for the whole application. It also defines a functional tests profile which allows developers to work on the application and build without invoking system integration tests (they can be slow depending on your application complexity). We can invoke the funstional test profile, for example, on our continuous integration server (continuum is used in this case). This a typical parent module with packaging type pom (<packaging>pom</packaging>). It has only two 'specific' for this whole task sections: the module and profile definition and compiler settings.

The module definition is pretty simple:


<modules>
<module>ejb_module</module>
<module>war_module</module>
<module>ear_module</module>
</modules>
<!--
Functional test, activation can be done by adding '-P functional-test' (no quotes) at the end of the command line e.g.:> mvn install -P functional-test
-->
<profiles>
<profile>
<id>functional-test</id>
<activation>
<property>
<name>enableCiProfile</name>
<value>true</value>
</property>
</activation>
<modules>
<module>test_module</module>
</modules>
</profile>
</profiles>


According to the configuration above the default execution of build wont include the building of functional (integration) tests module. If we want to include integration tests as part of our build we can append "-P functional-test" at the command line, e.g. mvn install -P functional-test. There are also other ways of triggering this profile.

And the last section of the parent module forces compiler to compile all java classes to java 1.4 compatible bytecode across all modules (because WAS-6 supports bytecode upto java 1.4):


<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.4</source>
<target>1.4</target>
</configuration>
</plugin>
</plugins>
</build>


Because all other modules declare this module as parent one they all use this setting so we don't have to specify it at every module that compiles java code.

Now, in order to generate RAD projects from our maven files we can use the following command:
mvn eclipse:rad
and maven eclipse plugin takes care of the rest. Now, to build the entire project we can invoke the following command:
mvn install -Duser.install.root="path to your WAS profile dir"


Where you can specify the path to your websphere profile dir which is needed to build stubs and sceletons for your EJBs (as specified in the EJB module section).
And finally, if we want to have a complete build including the integration tests that also deploy the application to the server we can issue the following command:

mvn install -P functional-test -Duser.install.root="path to your WAS profile dir"


This line looks pretty long so we can put in a command line script.


If there are any questions or constructive comments you are welcome to send them in! :)

15 comments:

Anonymous said...

Maybe http://mojo.codehaus.org/was6-maven-plugin can be of use for you?

Siarhei Dudzin said...

This definitely looks interesting! At the time of writing there were no plugins available.

Anonymous said...

it is really a great article.although i got this article after i was done with maven with same approach. i was thinking to document it,but i dont need to do so. it is more than enough. he has done a gre8 work. just gre8888888888888888

Anonymous said...

Would you include the ejbDeploy.xml code as well, as this is missing from your post.

Thanx

Siarhei Dudzin said...

It is not missing, it is just before the "Parent module" section :)

netslow said...

Do you know is there any possibility to set automatic redeploy after server starts? Maybe there is an option to set auto redeploy after (for example) 60 seconds?

Siarhei Dudzin said...

You could try Hot deployment and dynamic reloading

subhas said...

I am very new in Maven world.I want to develop J2EE project(EJB,WEB) using RAD 6 and Maven.After generate the RAD related file when I import WebModule(in eclipse way , in eclipse works fine) into RAD I got some build error and it's not allowed to deploy because it's not part of valid j2ee project.So I tried to import Enterprise module and it's imported properly but war included into the EAR.So I can't change the code.
Can any body give any clue how you are debug and fix in RAD of maven project.

Subhas

Siarhei Dudzin said...

For rad use eclipse:rad and eclipse:rad-clean goals. Do *not* put WTP 2.0 property in maven-eclipse-plugin configuration.

subhas said...

Thank you very much for quick reply.I used maven eclipse plugin for create RAD specific .project and others file.As you describe I put the following entry in my pom.xml

plugin
groupId::org.apache.maven.plugins
artifactId::maven-eclipse-plugin
configuration:
buildOutputDirectory :: ${basedir}/src/main/webapp/WEB-INF/classes

additionalBuildcommands
buildcommand::com.ibm.etools.ctc.serviceprojectbuilder
additionalBuildcommands

classpathContainers
classpathContainer::com.ibm.wtp.server.java.core.container/com.ibm.ws.ast.st.runtime.core.runtimeTarget.v60/was.base.v6

classpathContainers
configuration
plugin


But after creating the project related file When I tried to import into the RAD 6 it's not build properly , I got some popuup with not build properly ..


Can you provide some details how you import this project(maven project) into RAD 6 workspace.I used import -> import from exting project -> select path -> FiNISH

After build fail when I try to deploy into Test Server it's gave message like not valid J2EE project.

Siarhei Dudzin said...

This is exactly how I import the projects.

If you use web services I recommend to use default RAD directory layout (it can't always deal 'customized' source and web folders).

Dev_Chennai said...

Hi,
I want to install my project in Both WebSphere & Weblogic Servers.So I want to creat a server based maven build.Is it possible to create this.For example,If i want to build for WebSphere,I need ejbDeploy process & for Weblogic I don't need.Any one has any documents or ideas..?

Dev_Chennai said...

Hi,
I want to install my project in Both WebSphere & Weblogic Servers.So I want to creat a server based maven build.Is it possible to create this.For example,If i want to build for WebSphere,I need ejbDeploy process & for Weblogic I don't need.Any one has any documents or ideas

Kitti Kitas said...

Hi,

In the configuration modules section in the EAR module; the web module and ejb modules bundle file names are defined as:

web_module.war
..
ejb_module.jar

Can we include a maven project version there? like:
..
/myservicsWeb
myservicsWeb-1.0.war
..
myservicsEJB-1.0.jar

Because our war and ejb dependencies are with the version numbers. Please note that the web context name is without version number.

I am able to create the EAR with the maven version numbers as said above. But not sure whether it works when we deploy to WAS.

Any suggestions?

Thanks
KS

Siarhei Dudzin said...

@Kitti Kitas: may be with maven resource filtering you can achieve getting the right module names in the deployment descriptors.