Technical Blog

8 Posts tagged with the maven tag

I have a love/hate relationship with Eclipse.  It's a great editor with sane default key mappings, but it tries to be too much.  One thing that never worked all that well is the WTP plugin for managing application servers.


How many times have you refreshed/published/republished Tomcat in Eclipse?  Sometimes the configuration files get out of sync and you have to clean it.  And since all the wars end up in the same Tomcat instance the start up time is horrendous.


An alternative to WTP is the Tomcat Maven plugin.  It allows you to manage Tomcat servers, but what I want to show you is how use it to run a war project in an embedded Tomcat instance.


Unfortunately our war projects are not fully Mavenized (yet), but we can get around that by using the fact it is just a war in the end.  This allows us to use the Maven War plugin to create an overlay over our war projects and then run that in Tomcat.


So enough pre-amble, show me the code!  Let’s get com.elasticpath.sf running using the Tomcat Maven plugin and MySQL.


First we need to get some environment specific settings out of the way.  Since we are doing a war overlay we won’t have access to any of the settings in env.config.  Instead we’ll put our environment specific settings in ~/.m2/settings.xml:

<?xml version="1.0"?>

<settings>

  <activeProfiles>

    <acitveProfile>mysql-dev-db</activeProfile>

    <activeProfile>storefront-tomcat-properties</activeProfile>

    <activeProfile>keystore-properties</activeProfile>

  </activeProfiles>

                       

  <profiles>

    <profile>

      <id>storefront-tomcat-properties</id>

      <properties>

        <storefront.context>/storefront</storefront.context>

        <storefront.http.port>8080</storefront.http.port>

        <storefront.https.port>8443</storefront.https.port>

      </properties>

    </profile>

    <profile>

      <id>keystore-properties</id>

      <properties>

        <keystore.file>/your/very/own/.keystore</keystore.file>

        <keystore.pass>changeit</keystore.pass>

      </properties>

    </profile>

  </profiles>


       …


</settings>


Other war projects will need their own profile.  The profile mysql-dev-db will come from the grandparent pom.  It is available since 6.3 and the source is included under elasticpath-grandparent/pom.xml.  If the default properties defined in the mysql-dev-db profile are unsuitable for your needs, they can be overriden using another profile in ~/.m2/settings.xml and activate it after the mysql-dev-db profile.


We then create a new Maven project called storefront which will overlay com.elasticpath.sf.  Here we will use the released version of the grandparent pom and define the parent of the storefront project as the grandparent pom:

<?xml version="1.0" encoding="UTF-8"?>

<project

xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

  <modelVersion>4.0.0</modelVersion>


  <parent>

    <groupId>com.elasticpath</groupId>

    <artifactId>grandparent</artifactId>

    <version>16</version>

  </parent>

  <groupId>com.elasticpath.extensions</groupId>

  <artifactId>storefront</artifactId>

  <version>1.0-SNAPSHOT</version>

  <packaging>war</packaging>

  <name>Storefront Extension</name>


  <dependencies>

    <dependency>

      <groupId>com.elasticpath</groupId>

      <artifactId>com.elasticpath.sf</artifactId>

      <version>...your Elastic Path version...</version>

      <type>war</type>

    </dependency>

  </dependencies>


Now define the Tomcat Maven plugin configuration.  The path and port configuration is defined in the settings profile.  And finally we choose the JDBC driver.  Yes this means you never again have to figure out where is Tomcat’s shared library folder.  All the epdb.* properties are defined in the mysql-dev-db profile.

  <build>

    <plugins>

      <plugin>

        <groupId>org.codehaus.mojo</groupId>

        <artifactId>tomcat-maven-plugin</artifactId>

        <version>1.1</version>

        <configuration>

          <path>${storefront.context}</path>

          <port>${storefront.http.port}</port>

          <httpsPort>${storefront.https.port}</httpsPort>

          <keystoreFile>${keystore.file}</keystoreFile>

          <keystorePass>${keystore.pass}</keystorePass>

        </configuration>

        <dependencies>

          <dependency>

            <groupId>${epdb.maven.groupId}</groupId>

            <artifactId>${epdb.maven.artifactId}</artifactId>

            <version>${epdb.maven.version}</version>

          </dependency>

        </dependencies>

      </plugin>


We need to define the jdbc/epjndi resource as a MySQL datasource.  This is done with META-INF/context.xml.  To get our settings into context.xml we’ll filter it using the Maven War plugin:

      <plugin>

        <groupId>org.apache.maven.plugins</groupId>

        <artifactId>maven-war-plugin</artifactId>

        <version>2.1.1</version>

        <configuration>

          <webResources>

            <resource>

              <directory>src/main/webapp-filtered</directory>

              <filtering>true</filtering>

            </resource>

          </webResources>

        </configuration>

      </plugin>

    </plugins>

  </build>

</project>


That will filter any file in src/main/webapp-filtered and put the result in the root of the built war.  This allows us to create src/main/webapp-filtered/META-INF/context.xml with the contents:

<Context>

  <Resource auth="Container" name="mail/Session" type="javax.mail.Session" />

  <Resource

    name="jdbc/epjndi"

    auth="Container"

    scope="Shareable"

    type="${epdb.datasource.classname}"

    maxActive="100"

    maxIdle="30"

    maxWait="10000"

    removeAbandoned="true"

    username="${epdb.username}"

    password="${epdb.password}"

    driverClassName="${epdb.jdbc.driver}"

    url="${epdb.url}" />

</Context>


...and have it filled out by the mysql-dev-db profile.


Our storefront project depends on com.elasticpath.sf so make sure it's in your local repo by running ant install under com.elasticpath.sf.


Since we are running Tomcat with Maven in order to pass JVM arguments to Tomcat you’ll need to configure the environment variable MAVEN_OPTS instead of CATALINA_OPTS.  Note for Java 6 users to copy over their special JVM args to MAVEN_OPTS.  This also means remote debugging parameters should be set in MAVEN_OPTS.  For more information on remote debugging see: http://java.dzone.com/articles/how-debug-remote-java-applicat


Finally we run our storefront project with

    mvn tomcat:run-war


Storefront is now running under http://localhost:8080/storefront and can be killed with ^C.


Repeat for the remaining wars and you can have each war project running in a separate embedded Tomcat instance.  Just remember to give each instance its own port and update settings like COMMERCE/SYSTEM/SEARCH/searchHost.  See http://mojo.codehaus.org/tomcat-maven-plugin/index.html for more details on how to configure the run-war goal.  Special care is needed to ensure each project has its own remote debugging ports.  This is accomplished by setting different MAVEN_OPTS for each project.  Simply use

    MAVEN_OPTS=”$MAVEN_OPTS <project specific opts>” mvn tomcat:run-war

This will temporary append project specific options to MAVEN_OPTS.  There is no semicolon before mvn because we only want to set this variable for this process and not back to the shell.


This gives you great control to start and stop individual wars.  Only changed com.elasticpath.sf?  Just ant install and restart that Tomcat instance.


While it took a little bit of work to get the environment specific settings in the right place this now gives us a platform to use more Maven without having to wait for the full Mavenization of our projects.  Let’s get cracking!

0 Comments Permalink

One of the quirks that I used to have while developing using binary based development is the fact that we have multiple maven projects, and changes to one of the projects (ie. core) will need to be rebuilt in other projects that depend on it (ie. storefront).

 

This was typical what I had to do:

 

cd project-root
cd core-project
mvn clean install

<take a nap for a few minutes>

<check to make sure the project built successfully>

cd ../storefront-project
mvn clean install

 

The other option is to build all the projects in the workspace.

 

cd project-root
mvn clean install

<take a longer nap for 10 minutes>

 

That is actually ineffective and a waste of time. One could argue you can write a script that will invoke those commands for you. However, you'd have to check for cases whent the build fails. Luckily, I found that there is a maven plugin that helps with automating that for us.

 

This is where the Maven Reactor plugin comes to the rescue.

 

The solve the previous problem where you changed your core project and only want to build the storefront, all you have to do is type this in the project-root directory:

 

mvn reactor:make -Dmake.folders=storefront-project

 

The Maven Reactor plugin will build core-project and then the storefront-project for you. If there is a problem with the core project (ie. compilation failures), it will stop the build and tell you.

4 Comments Permalink

Overview:

Elastic Path 6.2.2 comes with a little known project called "com.elasticpath.test.application". This project is used as a connector to enable integration testing by exposing the spring application context to hook up Elastic Path core services for retrieving, updating, and removing EP domain objects. This article will go through the steps on how to use this project for integration testing.

 

Why?

 

Our client is using EP's web services to perform shopping related operations, and while we can test the web service calls with a framework like Groovy, what we also needed was the ability to check the backend to ensure the result of the web service call has been persisted in the database. We also needed to manipulate the data objects so we could test the edge cases through the web service. We could have written raw SQL statements to insert these rows, but then again you would have to insert the object graph of these domain objects. The downside to this is if one were to add a new column or remove an existing column, these SQL statements will have to be changed.

 

The Solution:

 

We rely on one of the projects that come packaged seperately in 6.2.2: com.elasticpath.test.application. This project will give us hooks into Spring and the service layer which we can then use to manipulate the domain objects. In addition, it provides us with a few Factory methods which we can use to create and persist certain domain objects for testing.This project was originally designed for use with FIT tests, but with a little bit of tweaking we can use it to our advantage.

 

The Setup:

 

- Elastic Path 6.2.2, taking advantage of the binary-based development approach

- Java 6

- Maven 2.2.1

 

The Details:

 

Step 1: Create your Integration test project.

 

In the pom file reference these jars. Here I am assuming you have a core extension project already, such that it is available for reference in this test project.

 

     <dependencies>
        ...
        <dependency>
            <groupId>com.elasticpath</groupId>
            <artifactId>com.elasticpath.test.application</artifactId>
            <version>6.2.2</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId><your group id></groupId>
            <artifactId><your core artifact id></artifactId>
            <version><your version #></version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>com.elasticpath</groupId>
            <artifactId>com.elasticpath.core.resources</artifactId>
            <version>6.2.2</version>
            <scope>compile</scope>
        </dependency>
          ...
      </dependencies>

 

NOTE: We need the com.elasticpath.core.resources package because it contains the out-of-the-box Spring bean definitions that we will use later.

Step 2: Copy the files from the com.elasticpath.core.itest project into our new project.

 

The reason we need this is because we need to set up the database configuration and spring definitions specific for integration tests. Copy the two files integrationtests/test_application.config and integration-core-context.xml into your Maven test project's src/main/resources folder.

 

Step 3: Modify your test_application.config file.

 

Change the setting in the file to correspond to the line below. We need to perform this change because all of our required spring definitions come from this file.

context.definitions_file=integration-core-context.xml

 

Step 4: Modify your integration-core-context.xml file.

 

Here we will need to do a couple of things. First, append this line to the end of the file:

    <import resource="classpath*:META-INF/conf/plugin.xml" />

 

This will import your definition from your core extension project.

 

The next step involves an examination of the integration-core-context.xml file. Due to the fact that there are still some FIT test references in this file, we will have to look for them and remove them as neccessary. Remove the beans with the following IDs:

 

importJobRunnerBaseAmount
importJobRunnerBaseAmountForFit
baseAmountDtoInsertUpdateImporterForFit
contentWrapperLoader
contentWrapperRepository
velocityRenderer
checkoutEventHandler

 

Step 5: Modify the com.elasticpath.test.application project.

 

A couple of modifications to the com.elasticpath.test.application project are required in order for us to use it. First off, we will need to add the following to TestApplicationContext.java:

 

A.) Change the variable TEST_APPLICATION_CONFIG_FILE to read as shown on the next line. This is because the location of the file has changed.

    private static final String TEST_APPLICATION_CONFIG_FILE = "test_application.config";

 

 

B.) Change the method overrideAllDefinitionsToLazy() to correspond to the code block below. We need to do this because we want to load the additional prototype beans defined by the BeanRegistrar.

    private void overrideAllDefinitionsToLazy() {
        for (int i = 0; i < beanFactory.getBeanDefinitionNames().length; i++) {
            String name = beanFactory.getBeanDefinitionNames()[i];
            if (name != null && !name.startsWith("beanRegistrar")) {
                AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition) beanFactory.getBeanDefinition(name);
                beanDefinition.setLazyInit(true);
            }
        }
    }

 

C.) Inside the initializeBeanFactory() method, make the change described below. We need to do this because we are now loading the file from the Classpath.

        beanFactory = new XmlBeanFactory(new FileSystemResource(definitionsFile));  

          to

        beanFactory = new XmlBeanFactory(new ClassPathResource(definitionsFile));

 

D.) Replace the method getApplicationProperties() with the following code block below. We need to do this because we are now loading the file from the Classpath.

    private Properties getApplicationProperties() {
        Properties properties = new Properties();
        try {
            InputStream inStream = this.getClass().getClassLoader().getResourceAsStream(TEST_APPLICATION_CONFIG_FILE);
            properties.load(inStream);
            
        } catch (IOException e) {
            throw new TestApplicationException("Failed to load properties from file:" + TEST_APPLICATION_CONFIG_FILE, e);
        }
        return properties;
    }

 

Step 6: Rebuild the com.elasticpath.test.application project by navigating to the project root directory and issuing the following command:

ant clean jar

 

Step 7: Now test that your integration project works by creating a test called MyTest.java under src/test/java in the new integration test project and copy the following into that file:

 

public class CreateWarehouseTest {
 
  private StoreTestPersister storeTestPersister;
 
  @Before
  public void setUp() {
    TestApplicationContext tac = TestApplicationContext.getInstance();
    tac.useDb();
 
    storeTestPersister = tac.getPersistersFactory()
        .getStoreTestPersister();
  }
 
  @Test
  public void testCreateWarehouse() {
    storeTestPersister.persistDefaultWarehouse();
  }

 

Step 8: Now configure your test_application.config file in your integration test project to point to your database and the initial database script location, and then run the test you just created.

 

db.script.dir=<your db script dir>

 

You should see the warehouse being persisted to the database.

0 Comments Permalink

If you're a savvy Maven user, you may already know about Maven's enforcer plugin, which lets you set rules for the environment a project is built in (e.g., JDK version, Maven version, OS family, even user-defined rules). If you're using it, you already know the key benefits, but have you really given it a good workout?

 

Last week, I found two handy options: requirePluginVersions and bannedDependencies. The first one makes sure that you've specified a version for each plugin you're using. Often, we just use the default of "LATEST". Then a few months into development, a plugin has a new release and our build starts to act... differently. With this setting enabled, Maven will fail your build if any of your plugins don't include a version!  (Make sure you check mvn site before you push your changes!)

<build>
...
<plugins>
...
 <plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-enforcer-plugin</artifactId>
  <executions>
   <execution>
    <id>enforce-build-environment</id>
   <phase>validate</phase>
   <goals>
    <goal>enforce</goal>
   </goals>
   <configuration>
    <rules>
....
    <requirePluginVersions>
     <message>Carefully select a version for all plugin dependencies.</message>
    </requirePluginVersions>

 

 

Remember to periodically run mvn versions:display-plugin-updates to see if new versions of your plugins are available.

 

Next, if you've tried switching away from commons-logging or log4j to use slf4j, you've probably found versions of those unwanted loggers showing up from time to time.  Sure, you can run mvn dependency:tree and see how it got there, but by that point it's too late. With bannedDependencies, Maven will walk your dependency graph and a banned dependency will cause your build to fail.  I found this blog entry which showed how to easily make Maven do the heavy lifting for me to keep log4j and commons-logging at bay.

<rules>
...
     <bannedDependencies>
      <searchTransitive>true</searchTransitive>
       <excludes>
        <exclude>log4j:log4j</exclude>
        <exclude>commons-logging</exclude>
        <exclude>org.slf4j:slf4j-log4j12</exclude>
        <exclude>org.slf4j:slf4j-jcl</exclude>
        <exclude>org.slf4j:slf4j-jdk14</exclude>
       </excludes>
       <message><![CDATA[Secondary logging frameworks are banned in preference to org.slf4j:*-over-slf4j.
Use mvn dependency:tree -Dincludes=log4j:log4j to find log4j (etc for commons-logging.)]]></message>
     </bannedDependencies>

 

 

The enforcer has some other built-in rules too. You already fail your build if your tests show your code was wrong, now you can fail your build if your build is wrong. Cool beans!

0 Comments Permalink

Before we start, you may speculate about the usefulness of class shadowing. Class shadowing can be used as a trick to patch or replace behaviors of classes at runtime, taking advantage of the first-come first-served algorithm of Java’s class loader. If there are more than one class with the same fully qualified name in the Java class loader, the first one that shows up always take precedence over the rest.

 

This is extremely useful in an upgrade project. For example, in order to make some out-of-the-box (OOTB) methods extensible without directly changing the source, or to provide backward-compatibility support, class shadowing is a good technique to separate the OOTB code and the customized code. By creating a patch jar against the OOTB jar and putting it before the OOTB jar on classpath, classes with the same qualified name are merged at runtime.

 

Class shadowing is only one way of patching a class. It’s based on class loader’s runtime class resolution. Another way is to use class overlay. For example, the Maven war overlays plugin expands the war file and copy them on top of the host classes.

 

If you are already familiar with OSGi based technology, patch fragment is actually taking advantage of the class shadowing mechanism. This post is not targeting at building OSGi application but at standard Maven project.

 

To instruct Java’s class loader to load jars in a specific order, we can make use of the “-cp” option. For example,

 

> java -cp patch.jar ootb.jar -jar main.jar

 

Besides, we can also make use of the Class-Path attribute of the jar manifest to specify the classpath. We will adopt this method in this post.

 

Here is an example we are going to build. HelloWorldProxy is a proxy artifact that exports its classpath in such an order that classes in HelloWorldPatch are replacing classes in HelloWorld. HelloWorldTest depends on HelloWorldProxy and doesn’t know which implementation(HelloWorld or HelloWorldPatch) HelloWorldProxy is exporting. The dependency graph is as followed:

 

Screen shot 2010-07-12 at 11.51.58 AM.png

In the pom.xml of HelloWorldProxy, it has two dependencies and we put HelloWorldPatch before HelloWorld, since we would like see classes in HelloWorldPatch replacing the ones in HelloWorld. As of Maven 2.0.9, the ordering of dependencies on the classpath is preserved. The code snippet is as followed:

 

<dependencies>
    <dependency>
        <groupId>HelloWorld</groupId>
        <artifactId>HelloWorldPatch</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <type>jar</type>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>HelloWorld</groupId>
        <artifactId>HelloWorld</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <type>jar</type>
        <scope>compile</scope>
    </dependency>
</dependencies>

 

We also need to make sure HelloWorldProxy exports the two jars in the Class-Path attribute of the MANIFEST.MF file with the correct ordering. The key is to set the addClasspath flag to true in the maven-jar-plugin:

 

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>2.3.1</version>
    <configuration>
        <archive>
            <manifest>
                <addClasspath>true</addClasspath>
            </manifest>
        </archive>
    </configuration>
</plugin>

 

Run “mvn package” in HelloWorldProxy and take a look at the generated MANIFEST.MF:

 

Screen shot 2010-07-12 at 11.56.55 AM.png

 

Voila! That’s what we expect! HelloWorldPatch takes precedence over HelloWorld on HelloWorldProxy’s classpath!

 

Now HelloWorldTest can safely depends on HelloWorldProxy and expects that HelloWorldProxy will export HelloWorldPatch’s implementations at runtime:

 


<dependency>
    <groupId>HelloWorld</groupId>
    <artifactId>HelloWorldProxy</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <type>jar</type>
    <scope>compile</scope>
</dependency>

 

The source of this example is available on GitHub http://github.com/jingweno/patching_with_class_shadowing_and_maven. You can also view it directly with CodeFaces http://codefaces.org/http://github.com/jingweno/patching_with_class_shadowing_and_maven.

0 Comments Permalink

(Or: How to Copy and Paste XML Your Way to Greatness)

 

We have a truckload of Selenium tests that poke and probe away at the user interface of our project here at EP. We realized it would be awesome if this testing could just automagically happen and let us know the results. So we took a look at our Maven build and realized the steps we wanted to automate were:

 

  1. Check out our source code, build the latest set of artifacts and deploy them to our internal Maven repository.
  2. Stop the web application running on our QA server environment.
  3. Recreate our test MySQL database with our default schema, apply any change sets needed to bring it to the latest version and load our QA data.
  4. Update the test data to reflect our QA server environment.
  5. Deploy our demo assets to a known location.
  6. Start our web application and wait for it to fully start.
  7. Launch Selenium and run our tests against our application on Firefox on our Linux Hudson slave.
  8. Record and announce the results of the tests.

 

In this blog post, I'm going to cover how we automated the last three steps.

 

The first thing we needed to do was find a tool that could automate configuring a web app container and deploying our WAR files. This tool would need to support multiple web containers without having to write custom scripts for each one. After doing some research, we decided to go with the fairly capable Cargo project. It has a fairly mature Maven plugin and good support for our preferred container, Tomcat, and it's come a long way since its inception. On our side, all we'd need to do is write a simple Groovy script to wait until our web application is in a ready-to-test state.

 

Our particular application really benefits from the new WebDriver features in Selenium 2.0, and while we'd love to take advantage of the Selenium Plugin for Maven, it's tied to Selenium 1.0, so we can't fully use it yet. We can however use it to bootstrap an X server on our Hudson build slave.

 

Our Selenium tests are written using TestNG and stored in their own Maven artifact. We can easily bind the Maven Failsafe Plugin to run during our integration-test phase, but we'd like to publish the output to our internal reporting site, so we can take a look at the last results. Luckily, we can use the Maven Surefire Report plugin to generate our site. Lastly, we'll get Hudson to announce the results of our test. (The page on the Maven build lifecycle is invaluable when figuring out when to run different plugins.)

 

So, let's get started!

 

Properties

 

To make Cargo and Selenium more useful, we pushed a handful of settings into a parent POM. This lets us override values and use the same artifacts for our own local development testing.

 

<properties>
    <cargo.jvmargs>-XX:MaxPermSize=512M -Xmx1024m</cargo.jvmargs>
    <cargo.servlet.port>8080</cargo.servlet.port>
    <cargo.tomcat.ajp.port>8009</cargo.tomcat.ajp.port>
    <cargo.rmi.port>1099</cargo.rmi.port>
    <cargo.tomcat.shutdown.port>8205</cargo.tomcat.shutdown.port>
    <cargo.wait>true</cargo.wait>

    <demo.hostname>10.9.8.7</demo.hostname>

    <searchserver.context>searchserver</searchserver.context>
    <ourapp.context>ourapp</ourapp.context>
    <cmserver.context>cmserver</cmserver.context>
    <storefront.context>storefront</storefront.context>

    <!-- Absolute -->
    <assets.directory>${user.home}/ep-assets</assets.directory>

    <jdbc.driver>com.mysql.jdbc.Driver</jdbc.driver>
    <jdbc.host>10.9.8.7:3306</jdbc.host>
    <jdbc.db>OURAPP_DB</jdbc.db>
    <jdbc.username>atwill</jdbc.username>
    <jdbc.password>grep</jdbc.password>
</properties>

 

Okay, that's more than a handful. but let's go over them really quickly. The cargo.* properties are there to let us easily have multiple Tomcats running on the same IP, the *.context properties tell Cargo where to deploy our various WAR files. The cargo.wait property tells Cargo to pause after it starts the application server. If set to false, Cargo will proceed along the Maven build lifecycle.  The assets.directory is used by steps 4 and 5.  The demo.hostname is the public hostname (or IP in our case) for our deployment (you'll see why we do this as we move along). The jdbc.* properties describe our JDBC DataSource. We've broken the information up so that we can use it in steps 3, 4 and 6. As you can imagine, it's pretty easy to override these properties in your local settings.xml and use Cargo to start a local app server for you - lucky you!

 

Containing the Excitement

 

Our first goal is to start a container, deploy our artifacts and wait for them to all start. For our situation, we felt it was best to have a separate container Maven artifact (with pom packaging) dedicated to steps 5 and 6. So, first we'll declare the WAR and JAR files that we depend on for the container. We included the JDBC driver and the JAI JARs for our Storefront too. We've already declared the versions for all these artifacts in a parent POM.

 

....            
           <dependencies> 
                <!-- WARs to be deployed by Cargo (also add them in cargo below) -->
                <dependency>
                        <groupId>com.elasticpath.mdm</groupId>
                        <artifactId>ourapp-war</artifactId>
                        <type>war</type>
                </dependency>

                <dependency>
                        <groupId>com.elasticpath.mdm</groupId>
                        <artifactId>searchserver</artifactId>
                        <type>war</type>
                </dependency>

                <dependency>
                        <groupId>com.elasticpath.mdm</groupId>
                        <artifactId>cmserver</artifactId>
                        <type>war</type>
                </dependency>

                <dependency>
                        <groupId>com.elasticpath.mdm</groupId>
                        <artifactId>storefront</artifactId>
                        <type>war</type>
                </dependency>

                <!-- Dependencies provided to Cargo -->
                <dependency>
                        <groupId>mysql</groupId>
                        <artifactId>mysql-connector-java</artifactId>
                        <scope>provided</scope>
                </dependency>

                <dependency>
                        <groupId>javax.media</groupId>
                        <artifactId>jai-core</artifactId>
                        <scope>provided</scope>
                </dependency>

                <dependency>
                        <groupId>com.sun.media</groupId>
                        <artifactId>jai-codec</artifactId>
                        <scope>provided</scope>
                </dependency>
        </dependencies>

 

Next up, we'll start Cargo in the pre-integration-test phase and stop it when we hit the post-integration-test phase. If you're copying and pasting this XML, you'll want to paste the next three sections in sequence into your POM, but we'll cover them individually.

<build>
    <plugins>
        <plugin>
            <groupId>org.codehaus.cargo</groupId>
            <artifactId>cargo-maven2-plugin</artifactId>
            <version>1.0.1-beta-2</version>
            <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>

            <configuration>
                <container>
                    <dependencies>
                        <dependency>
                            <groupId>mysql</groupId>
                            <artifactId>mysql-connector-java</artifactId>
                        </dependency>

                        <dependency>
                            <groupId>javax.media</groupId>
                            <artifactId>jai-core</artifactId>
                        </dependency>

                        <dependency>
                            <groupId>com.sun.media</groupId>
                            <artifactId>jai-codec</artifactId>
                        </dependency>
                    </dependencies>

                    <containerId>tomcat5x</containerId>
                    <zipUrlInstaller>
                        <url>file:${project.basedir}/apache-tomcat-5.5.29.zip</url>
                        <installDir>${installDir}</installDir>
                    </zipUrlInstaller>
                </container>

 

In the previous example, we bind the plugin to the lifecycle and tell Cargo to include the JDBC driver and JAI JARs into the classpath. (We won't get native acceleration for JAI, but that's fine for our purposes.) You'll probably notice the tricky thing we did for the zipUrlInstaller. To remove a dependency from our startup, we've checked in a copy of Tomcat into our project. Let's take a look at the Cargo configuration:

<configuration>
    <home>${project.build.directory}/tomcat5x/container</home>
    <properties>
        <cargo.jvmargs>${cargo.jvmargs}</cargo.jvmargs>
        <cargo.logging>high</cargo.logging>
        <cargo.servlet.port>${cargo.servlet.port}</cargo.servlet.port>
        <cargo.tomcat.ajp.port>${cargo.tomcat.ajp.port}</cargo.tomcat.ajp.port>
        <cargo.rmi.port>${cargo.rmi.port}</cargo.rmi.port>
        <cargo.tomcat.shutdown.port>${cargo.tomcat.shutdown.port}</cargo.tomcat.shutdown.port>
        <!--
            Ampersands must be escaped such that they show up as "&" in
            the resulting XML file, so we use "&amp;" here.
        -->
        <cargo.datasource.datasource>
            cargo.datasource.url=jdbc:mysql://${jdbc.host}/${jdbc.db}?AutoReconnect=true&amp;amp;useUnicode=true&amp;amp;characterEncoding=utf-8|
            cargo.datasource.driver=${jdbc.driver}|
            cargo.datasource.username=${jdbc.username}|
            cargo.datasource.password=${jdbc.password}|
            cargo.datasource.type=javax.sql.DataSource|
            cargo.datasource.jndi=jdbc/epjndi
        </cargo.datasource.datasource>
    </properties>

Here we're passing in the cargo.* properties from our parent POM and we're asking Cargo to create a DataSource for us with the jdbc.* properties and add it to the JNDI tree.  For our situation, we're always using MySQL, so we can make a handful of assumptions in the URL.  Now that we've picked a container and configured it, we'll tell Cargo which artifacts we'd like to have deployed:

            <deployables>
                <!-- Applications to Deploy -->
                <deployable>
                    <groupId>com.elasticpath.mdm</groupId>
                    <artifactId>ourapp-war</artifactId>
                    <type>war</type>
                    <properties>
                            <context>/${ourapp.context}</context>
                    </properties>
                    <pingURL>http://${demo.hostname}:${cargo.servlet.port}/${ourapp.context}
                    </pingURL>
                    <pingTimeout>60000</pingTimeout>
                </deployable>
                <deployable>
                    <groupId>com.elasticpath.mdm</groupId>
                    <artifactId>searchserver</artifactId>
                    <type>war</type>
                    <properties>
                            <context>/${searchserver.context}</context>
                    </properties>
                    <pingURL>http://${demo.hostname}:${cargo.servlet.port}/${searchserver.context}/product/select?q=*:*
                    </pingURL>
                    <pingTimeout>60000</pingTimeout>
                </deployable>
                <deployable>
                    <groupId>com.elasticpath.mdm</groupId>
                    <artifactId>cmserver</artifactId>
                    <type>war</type>
                    <properties>
                            <context>/${cmserver.context}</context>
                    </properties>
                    <pingURL>http://${demo.hostname}:${cargo.servlet.port}/${cmserver.context}/
                    </pingURL>
                    <pingTimeout>60000</pingTimeout>
                </deployable>
                <deployable>
                    <groupId>com.elasticpath.mdm</groupId>
                    <artifactId>storefront</artifactId>
                    <type>war</type>
                    <properties>
                            <context>/${storefront.context}</context>
                    </properties>
                    <pingURL>http://${demo.hostname}:${cargo.servlet.port}/${storefront.context}/
                    </pingURL>
                    <pingTimeout>60000</pingTimeout>
                </deployable>
            </deployables>
        </configuration>
    </configuration>
</plugin>

 

This is pretty much textbook Cargo; you'll see our use of demo.hostname above in PingURL. Cargo will fetch the artifacts and deploy them into Tomcat waiting for the specified URL to not return an error, waiting up to pingTimeout milliseconds. Since we're just a demo application, the PingURL for the searchserver does a query for products. The problem here is that, while the searchserver may be available, the search indexes might not be built yet. So, we'll need something to check that the searchserver is not only started, but that its indexes are populated.

 

For this, we put together a simple Groovy script that waits until the searchserver returns products when queried. We put this in src/main/script/waitforstartup.groovy and it looks a little something like this:

import java.net.URL
import groovy.xml.StreamingMarkupBuilder


 def url = project.properties['searchserverUrl'];
 def timeout = project.properties['timeout'].toLong();

 /* For standalone testing, use something like:
  * 
  *     def url = "http://localhost:8080/searchserver/product/select?q=*:*"
  *     def timeout = 120*1000;
  */

def ready = false;
def text = "";
def first = true;

def begin = System.currentTimeMillis()

while (!ready) {
        
        if (System.currentTimeMillis() > (begin+timeout)) {
                // provided by gmaven
                fail("Timeout of "+timeout+"ms reached waiting for "+url+" to return at least one search result.")
        }
        
        if (!first) {
                Thread.sleep(10*1000);
        } else {
                first = false;
        }
        
        URL searchServer;
        try     {
                searchServer = new URL(url);
                text = searchServer.getText();
        } catch (e) {
                print "Server not yet ready, sleeping for 10 seconds. ("+e+")\n"
                continue;
        }
        
        if (text.size() == 0) {
                print "Not yet ready, sleeping for 10 seconds. (Empty HTTP response received)\n"
                continue;
        }
        
        try {
                def root = new XmlSlurper().parseText(text)
                
                if (numFound(root)!=0) {
                        ready = true;
                } else {
                        print "No elements returned in search result, sleeping for 10 seconds.\n"
                        continue;
                }
        } catch (e) {
                print "Trouble parsing XML result, sleeping for 10 seconds. ("+e+")\n"
                continue;
        }
}

def numFound(root) {
        return root.result.@numFound;
}

 

 

XmlSlurper was pretty neat (as used in numFound). Running it is pretty trivial with the GMaven plugin. You'll notice how we can easily pass in properties to the Groovy script via project.properties.

 

<plugin> 
    <groupId>org.codehaus.groovy.maven</groupId>
    <artifactId>gmaven-plugin</artifactId>
    <configuration>
        <properties>
            <timeout>300000</timeout>
            <searchserverUrl>http://${demo.hostname}:${cargo.servlet.port}/${searchserver.context}/product/select?q=*:*</searchserverUrl>
        </properties>
        <source>${basedir}/src/main/script/waitforstartup.groovy</source>
    </configuration>
</plugin>

 

The source element is relative to the current working directory, not your project's base directory, so it's a good idea to put in ${basedir} to prevent surprises later on.

 

Running mvn integration-test should scroll for a while as Maven downloads all required dependencies and eventually the container will start. If we run gmaven:execute, our Groovy script will run and happily poll our searchserver until it's ready!

 

This alone is a great set up for running builds of our project both on my development box and for our QA server!

 

In Hudson, I have one Job which spawns both targets in parallel.  The integration-test job never returns (until it's killed in step 2), but once the gmaven:execute target returns, it runs the Selenium tests.  Let's talk about those now...

 

Prepare for Take Off

 

Our second goal is to launch Selenium, run our tests and shut Selenium down.  It made sense for us to have a separate selenium-tests artifact for that purpose, let's start by taking a look at the properties we decided to set:

 

<properties>
    <selenium.version>2.0a4</selenium.version>
    <selenium.host>127.0.0.1</selenium.host>
    <selenium.port>14444</selenium.port>
    <selenium.DISPLAY>:17</selenium.DISPLAY>
    <selenium.background>true</selenium.background>
    <xvfb.option>-ac</xvfb.option>
    <seleniumUrl>http://${selenium.host}:${selenium.port}/wd/hub</seleniumUrl>
    <appUrl>http://${demo.hostname}:${cargo.servlet.port}/${ourapp.context}</appUrl>
</properties>

 

We're making use of the Maven Selenium Plugin to start up Xvfb on display :17, and we disable access control to Xvfb to work around any xauth issues we may run into.  Here you see another use of demo.hostname, passing it to Selenium as the application under test.  The selenium.background property allows us to run Selenium on the command line and keep the server running with -Dselenium.background=false, this running these tests during development makes testing a lot quicker!

 

Next we'll specify the dependencies we need to make this all happen:

<dependencies>
    <dependency>
        <groupId>org.seleniumhq.selenium</groupId>
        <artifactId>selenium</artifactId>
        <version>${selenium.version}</version>
    </dependency>

    <dependency>
        <groupId>org.testng</groupId>
        <artifactId>testng</artifactId>
        <version>5.11</version>
        <scope>test</scope>
        <classifier>jdk15</classifier>
    </dependency>

    <dependency>
        <groupId>org.seleniumhq.selenium</groupId>
        <artifactId>selenium-server</artifactId>
        <version>${selenium.version}</version>
    </dependency>
</dependencies>   
<repositories>
    <repository>
        <id>selenium-repository</id>
        <url>http://selenium.googlecode.com/svn/repository/</url>
    </repository>
</repositories>

No real surprises here, although I'm adding an extra repository just to pull down Selenium 2.0a4. You could easily mirror it locally in your own company-wide repository.

 

Now we need to start and stop Selenium.  Normally we'd use the Maven Selenium Plugin, but it's pinned to the older generation of Selenium. Instead of trying to be overly clever, we just ask Ant to start and stop Selenium during the pre-integration-test and post-integration-test phases, we pass in a DISPLAY variable which is ignored on non-Unix operating systems.

<build>
    <plugins>
        <plugin>
            <!--
                Use Ant to start and stop the selenium server - once the selenium
                plugin handles 2.0 we can get rid of this.
            -->
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-antrun-plugin</artifactId>
            <executions>
                <execution>
                    <id>start-selenium</id>
                    <phase>pre-integration-test</phase>
                    <configuration>
                        <tasks>
                            <echo taskname="start-selenium"
                                message="Starting Selenium Server v${selenium.version} on ${selenium.port} offering display ${selenium.DISPLAY}" />
                            <java taskname="start-selenium"
                                jar="lib/selenium-server-standalone-${selenium.version}.jar"
                                fork="true" spawn="${selenium.background}">
                                <env key="DISPLAY" value="${selenium.DISPLAY}" />
                                <arg line="-timeout 30 -debug -browserSideLog -port ${selenium.port}" />
                            </java>
                        </tasks>
                    </configuration>
                    <goals>
                        <goal>run</goal>
                    </goals>
                </execution>
                <execution>
                    <id>stop-selenium</id>
                    <phase>post-integration-test</phase>
                    <configuration>
                        <tasks>
                            <echo message="Stopping Selenium Server" />
                            <get taskname="stop-selenium"
                                src="http://${selenium.host}:${selenium.port}/selenium-server/driver/?cmd=shutDown"
                                dest="${project.build.directory}/selenium-shutdown.txt"
                                ignoreerrors="true" />
                        </tasks>
                    </configuration>
                    <goals>
                        <goal>run</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>

 

Okay, so we've configured Selenium to start and stop, now lets ask it to run our tests. We created an extremely simple TestNG suite in src/test/it/testng.xml:

<suite name="integration-tests">
     <test name="Everybody Everybody">
          <packages>
               <package name="com.elasticpath.mdm.tests.it" />
          </packages>
     </test>
</suite>

 

By convention, the Maven Failsafe Plugin will run src/test/java/<above packages>/*IT.java tests when called with integration-test and TestNG will look for methods annotated with @Test.  So back in the POM, we say:

    <plugin>
        <!--
            Run tests specified in the testng.xml file for integration-tests
        -->
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-failsafe-plugin</artifactId>
        <version>2.5</version>

        <configuration>
            <suiteXmlFiles>
                <suiteXmlFile>src/test/it/testng.xml</suiteXmlFile>
            </suiteXmlFiles>
            <systemPropertyVariables>
                <seleniumUrl>${seleniumUrl}</seleniumUrl>
                <appUrl>${appUrl}</appUrl>
            </systemPropertyVariables>
        </configuration>

        <executions>
            <execution>
                <id>integration-test</id>
                <phase>integration-test</phase>
                <goals>
                    <goal>integration-test</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
</plugins>
 

 

We bind it to the integration-test phase so it's run after Selenium starts. You'll notice that we pass seleniumUrl and appUrl into the TestNG tests. We pass those as parameters into a @BeforeSuite method; we'll touch on that in a moment.

 

Last bit, we're going to launch Xvfb, but only if we're on a UNIXy operating system, so we'll wrap it in a profile stanza:

<profiles>
    <profile>
        <id>xvfb-started</id>
        <activation>
            <os>
                <family>unix</family>
            </os>
        </activation>
        <build>
            <plugins>
                <plugin>
                    <!--
                        Before running the Selenium server, start a Xvfb to display
                        browsers into.
                    -->
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>selenium-maven-plugin</artifactId>
                    <executions>
                        <execution>
                            <id>xvfb</id>
                            <!--
                                We want to start Xvfb *before* pre-integration-tests, but the
                                only phase before pre-integration-test is package, so we put it
                                there.
                            -->
                            <phase>package</phase>
                            <goals>
                                <goal>xvfb</goal>
                            </goals>
                            <configuration>
                                <display>${selenium.DISPLAY}</display>
                                <options>
                                    <option>${xvfb.option}</option>
                                </options>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

Oh the hack.  Ironically, Maven does not have a way to specify that a particular execution id depends on another execution id, so we had to bind launching Xvfb to the package phase. Maybe in Maven 3.0?

 

Getting into the actual code, earlier I mentioned the @BeforeSuite attribute, in that method we'll configure Selenium for our purposes:

.....
 
        /*
         * When running TestNG from within Eclipse, the plugin passes this value in to parameters it cannot substitute.
         */
        private static final String PARAMETER_NOT_FOUND = "not-found";
.....
        /*
         * Passing in these properties will allow you to use the specified values if the parameters are not found by TestNG.
         */
        public static final String SELENIUM_URL_PROPERTY = "selenium.url";
 
        public static final String APPLICATION_URL_PROPERTY = "app.url";
 
        private static WebDriver driver;
 
        private static WebDriverBackedSelenium selenium;
 
        private static String sessionString;
        private static String appUrl;
.....
        @BeforeSuite
        @Parameters( { "seleniumUrl", "appUrl" })
        public void startBrowser(String seleniumUrl, String appUrl) throws Exception {
                
                if (PARAMETER_NOT_FOUND.equals(seleniumUrl)) {
                        seleniumUrl = System.getProperty(SELENIUM_URL_PROPERTY);
                }
                if (PARAMETER_NOT_FOUND.equals(appUrl)) {
                        appUrl = System.getProperty(APPLICATION_URL_PROPERTY);
                }
 
                driver = new RemoteWebDriver(new URL(seleniumUrl), DesiredCapabilities.firefox());
 
                // This will cause all find-element operations to keep trying for up to 10 seconds automatically.
                driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
 
                selenium = new WebDriverBackedSelenium(driver, appUrl);
 
                /*
                 * Generate a unique string so if we update a field to a new value, we know it's going to be a new value (and can compare that it's so).
                 */
                sessionString = System.currentTimeMillis() + "";
 
                selenium.open(appUrl);
 
        }
....
        @AfterSuite
        public void stopBrowser() {
                driver.close();
 
        }
.....
 

This method is called before our test suite runs.  We do some extra work to allow developers to run TestNG with -Dselenium.url and -Dapp.url if running using the TestNG plugin for Eclipse to pass in parameters.  We also configure WebDriver to automatically wait 10 seconds for events to occur.  Don't forget to tell people writing tests that they don't need to do this again in their code!

 

This configuration gives us a handful of options, developers can boot up a Selenium server by issuing "mvn -Dselenium.background=false integration-test", then run tests either in Eclipse or by calling surefire directly with "mvn surefire:integration-test".

 

 

Reporting the News

 

To recap, we can start a container, deploy our application, wait for it to start, then launch Selenium, run some tests and shut Selenium down when our tests have finished.  Our final goal is to report an announce the results of our test. Generating reports is pretty easy, we'll just add the following to our POM:

<reporting>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-report-plugin</artifactId>
            <version>2.5</version>
            <reportSets>
                <reportSet>
                    <id>integration-tests</id>
                    <reports>
                        <report>report-only</report>
                    </reports>
                    <configuration>
                        <outputName>failsafe-report</outputName>
                        <reportsDirectories>
                            <reportsDirectory>${project.build.directory}/failsafe-reports</reportsDirectory>
                        </reportsDirectories>
                    </configuration>
                </reportSet>
            </reportSets>
        </plugin>
    </plugins>
</reporting>
 

 

Wow, that was easy.  In Hudson, we configure the selenium-tests project to run the following Maven Goals: integration-test site-deploy failsafe:verify. This asks Maven to perform the tests, deploy the site and then fail if the tests failed.

 

I've also configured Hudson to send an email out if this build fails, that email contains the URL directly to the Selenium Failsafe report!

 

Now we have an automated deployment mechanism, Selenium tests and reporting.

 

Awesome!

0 Comments Permalink

The contract negociation went through, the SLA is signed, and as a super-keen developer, you've already downloaded and built Elastic Path within minutes ()! Your heart is racing. The adrenaline is pumping. You just can't wait to start building your ecommerce storefront. With the source code in hand, it's so tempting to start hacking the Elastic Path source code right away.

 

But then you start thinking about the next EP release version, and bug fixes... and you're wondering what you're going to do when it's time to upgrade... How will you manage your customizations while keeping in sync with the most recent stable release of Elastic Path?

 

Maven to the rescue! You can use the power of Maven dependencies and, more importantly, the WAR overlay feature to do this. Overlays are used to share common resources across multiple web applications. Basically, it's a nice and clean way to apply the Decorator pattern to any EP based web applications.

 

How It Works

Step 1. If you don't already have one, install your own Maven repository (Sonatype Nexus, for instance). I won't go into details, but have a look at this old post on TheServerSide.com: Setting Up a Maven Repository.

 

Step 2. Deploy all required libraries shipped with the EP source code in your new Maven repository and build the codebase as per the Developer Guide. When it's done, you should have all applications packaged and ready to be deployed. Depending on your chosen Maven repository, you will need to deploy your different WAR packages (cmserver, searchserver and storefront) by uploading both the WAR binary and the POM file within the project.

 

Step 3. Create a WAR Maven project to hold all the customizations that you want to make on the standard storefront source code (controller, XML configuration, etc.) and specify that you are dependent on EP's Mavenized storefront WAR as follows:

<dependency>
     <groupId>com.elasticpath</groupId>
     <artifactId>com.elasticpath.sf</artifactId>
     <version>6.1.1</version>
     <type>war</type>
</dependency>

 

In your POM project file, add   the Maven Warpath plugin. The Warpath plugin extends the existing war overlay functionality included in the Maven War plugin to turn war artifacts into fully fledged build dependencies. We need it to expose the classes built and contained within the WAR dependency.

<plugin>
     <groupId>org.appfuse</groupId>
     <artifactId>maven-warpath-plugin</artifactId>

     <extensions>true</extensions>
     <executions>
          <execution>
            <goals>
                 <goal>add-classes</goal>
            </goals>
          </execution>
     </executions>
</plugin>

 

While packaging your new extended storefront, all configuration, classes, and libs will be imported from the depending WAR archive into your new web application. The web application "overlay" will work exactly as the original and will be ready for any customization.
Now, let's say you want to override a specific configuration file like WEB-INF/web.xml to declare your extending ContextConfigListener java class. Simply copy locally to your project the WEB-INF/web.xml file and make your modifications. While packaging, the override files will replace all existing configuration files and resources from EP.

 

waroverlay.png

 

WAR overlay gives you full control and dependency management on all your EP customizations. In case of an upgrade or bug fix, all you need to do is rebuild the default EP package and update your project dependency. No more code merge nightmares! No more wondering if you missed any updated configuration files!

 

By combining the WAR overlay approach with Spring framework's plugin capability available within EP, you can override even bean implementation by simply creating your own core extension jar project, having a dependency on the standard EP core jar library, and defining a plugin.xml file within your own jar project. All this has already been proven in the field. You can implement your own storefront without modifying any EP core domain interfaces, services by injection you own implementation.

 

And voila! By leveraging Maven dependency and WAR overlay mechanisms, you have a clean, straightforward approach to implementing EP in the field that keeps the upgrade effort low.

8 Comments Permalink

Using Maven (http://maven.apache.org/) for dependency management frees developers from having to manually download and manage third-party dependencies. This greatly simplifies the process of upgrading third-party libraries and gives developers more time to work on actual development. Maven provides a number of features to simplify dependency management. The core concept is the idea of project object model files (poms). Poms are a standardized way to describe a project: group, artifact, name, version, dependencies, layout, etc.

Inheritance of Dependency Versions

Versions of third-party libraries can be specified in a high-level/parent pom. This allows subprojects that need a certain dependency to leave the version empty and use the one specified in the parent. Upgrading all projects to use a newer third-party library is as simple as changing one version number in the parent pom.

Transitive Dependencies

Since each dependency specifies its own dependencies, only direct dependencies of a project need to be specified. Any other runtime dependencies of those dependencies will be included automatically. i.e. If project A specifies B as a dependency and B requires C and D to run, then A will automatically get B, C, and D.

Dependency Fetching

Maven also includes automatic downloading and caching of dependencies. As new dependencies are required during the build process, Maven will check for a local copy. If there is no local copy, Maven will automatically download it.

In order to move from Antlion to Maven, we need to make a number of changes in our build scripts and configuration files:

  • The old jars.xml file has been removed and replaced by a dependency management section in our top-level pom, elasticpath.pom
  • All of the per-project dependency listings in the artifact.xml files have been removed and placed in pom.xml files within each project

 

For the most part, developers do not need to know much about Maven; it operates transparently behind Ant. However, when you need to change or add dependencies, some knowledge of the setup is required.

 

At Elastic Path, we have an internal Maven repository, managed by an instance of Archiva (http://archiva.apache.org). It not only provides a place to keep custom/missing dependencies, but also acts as a proxy to various public Maven repositories. If a newly added dependency is available in the public repositories, it will be automatically fetched and cached by Archiva.

Adding New jars

When a dependency cannot be found in any public repositories, or when a dependency has been patched or customized, it will need to be manually deployed to Archiva. At present, this requires Maven to actually be installed on the computer doing the deployment. Then the following steps need to be performed.

 

1. Create a pom for the jar if none exists. If the jar was patched or customized, append something to the version to indicate this (ex. 2.5.5-clientA-projectX-1.0).

2. Choose the repository to deploy to:

 

  • Internal - artifacts available from public Maven repositories.
  • Extras - artifacts not found in a Maven repository.
  • Custom - open-source libraries that have been patched or customized.
  • Restricted - libraries that are not redistributable, but are required internally during development and testing.
  • Other - if you are working on a project that is customizing the EP platform you should probably have a repository (or repositories) created specifically for your project which will make it easier to tell which libraries you need to redistribute.

 

3. Edit ~/.m2/repository/settings.xml file to specify your Archiva username and password.

<servers>
<server>
 <id>deployment.webdav</id>
 <username>{archiva-deployment-user}</username>
 <password>{archiva-deployment-pwd}</password>
</server>
</servers>

 

4. Create a new folder from where you will deploy the jar and create a pom there with the following content:

<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>webdav-deploy</artifactId>
<packaging>pom</packaging>
<version>1</version>
<name>Webdav Deployment POM</name>

<build>
 <extensions>
 <extension>
 <groupId>org.apache.maven.wagon</groupId>
 <artifactId>wagon-webdav</artifactId>
 <version>1.0-beta-2</version>
 </extension>
 </extensions>
</build>
</project>

 

5. Navigate on the command line to the folder created in the previous step and run the following command.

 

mvn deploy:deploy-file -Dfile=filename.jar -DpomFile=filename.pom

-DrepositoryId=deployment.webdav

-Durl=dav:http://maven.elasticpath.net/archiva/repository/{repository}

Setting up a Project

To set up a new project, you need to create a pom file.

 

1. Copy an existing pom to use as a starting point (com.elasticpath.cm/pom.xml for a new web project or com.elasticpath.ql/pom.xml for a jar).

2. Modify the artifactId, packaging, name, and description to reflect the new project.

3. Leave the parent section alone as it will allow you to inherit settings from the main EP pom.

4. Remove all the dependencies that are not required, and add any new ones that are needed.

5. Review the build section and modify the directories as required.

 

Since we are still using Ant to do the actual building, the build.xml and artifact.xml files will also have to be created. Copy and modify existing ones from the project you copied the pom from.

Using Proper pom Versions

If any modifications are made to an existing project, make sure to open the pom.xml and modify the version string. If there is no version setting, the project was inheriting the parent pom's version and one will have to be added. During active development the version string for a new or modified project should be something like 6.1-clientA-projectX-SNAPSHOT. For each release the version should be changed to something like 6.1-clientA-projectX-1.0 and then ...-1.1, ...-1.2, etc. This tells anyone looking at it exactly which EP release it was based on and for which client/project it was for.



0 Comments 0 References Permalink