One of the pain points that can arise during a development project is the payment gateway's support of recycled order numbers. Live production gateways will track all historical order numbers and generate a duplicate order number error if an existing order number is reused. This is fine in production but troublesome when the payment gateway test servers also behave in this manner. Most payment gateway providers I have worked with in the past offer duplicate checks within an hour or two on their test servers, but beyond that, order numbers can be recycled which is a good thing for development purposes. Some providers, however, appear to extend this duplicate check to a day, week, or may even provide no support for order number reuse.


If you use the database population scripts that come with Elastic Path to facilitate schema/data changes, this limitation can frustrate your development team, who will most likely end up manually tracking used order numbers and making frequent updates to TORDERNUMBERGENERATOR.


If your payment gateway provider doesn't support order number reuse or at least not to the frequency you desire, what can you do?

 

Some Alternatives

The simplest approach would be to externalize the initial order number to the env.config and assign everyone a unique order number block. The problem with this is that each environment would have to keep track of the last order number used and update the env.config with the next number in the block with every new database build.


Another approach would be to externalize the initial order number (or perhaps even the order number incrementer) to a central database. This database would have a simple mapping of environment to next order number and each environment would have a sufficient block assigned to last the duration of the project. The problem with this approach is that many teams/environments are distributed and you may not be able to set up a central database which can be accessed by all environments.

 

A Better Solution

A better solution (though not 100% foolproof) is to construct the initial order number during the database build process using a unique element of the environment as well as a random element to get around frequent rebuilds on the same environment. To do this, we can create a custom Ant task that takes a fixed component (derived from either a fixed prefix or the environment's IP address) and appends a randomly generated number. If the random number is sufficiently large, the likelihood of duplicate hits will be minimal.

 

Creating the Ant Task

The following is an example of a custom Ant task that accepts min and max parameters for the random number component and a prefix. Regardless of whether a fixed prefix or an environment's IP address was provided, the task will use the hash code of the fixed value for the resulting order number in order to work with the out of the box numerical order number incrementer.

public class OrderNumberGeneratorTask  extends Task {
    private String min = null;
    private String max = null;
    private String property = null;
    private String prefix = null;
    private Random random = new Random(System.currentTimeMillis());
 
    @Override
    public void execute() throws BuildException {
         String localPrefix = null;
        if (min == null || min.equals(""))
            throw new BuildException("Min property is missing.");
 
        if (max == null || max.equals(""))
            throw new BuildException("Max property is missing.");
 
        int minInt = Integer.parseInt(min);
        int maxInt = Integer.parseInt(max);
 
        if (minInt > maxInt)
            throw new BuildException("Min is bigger than max.");
 
        localPrefix = prefix;
        if (localPrefix == null) {
             try {
                  localPrefix = InetAddress.getLocalHost().getHostAddress();
               } catch (UnknownHostException e) {
                    throw new BuildException("Unable to get local host address.", e);
               }
        }
        int randomInt = calculateRandom(minInt, maxInt);
 
        getProject().setNewProperty(property, String.valueOf(localPrefix.hashCode()) + String.valueOf(randomInt));
    }
 
    protected int calculateRandom(final int minInt, final int maxInt) {
        return minInt + random.nextInt(maxInt - minInt + 1);
    }

 

Updating the Build Process

To use this task, we need to package the compiled class in a jar and add it to the Maven repository along with an entry in the ant/setup/pom.xml so that it gets included in the ant setup script. Once the class is available on the Ant class path, we add the following task definition and task invocation to the ant/default.xml setup file. This will create the order number and store it in the ep.initial.order.number variable.

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

<project name="ant_default" xmlns:artifact="antlib:org.apache.maven.artifact.ant" xmlns:contrib="antlib:net.sf.antcontrib">

  <taskdef name="ordernumbergenerator" classname="com.elasticpath.antextension.OrderNumberGeneratorTask"></taskdef>
... 
  <ordernumbergenerator min="1" max="1000000" property="ep.initial.order.number"></ordernumbergenerator>
...

 

Now we can use this variable in the database/src/base-insert.xml.vm file as the initial order number of the TORDERNUMBERGENERATOR table.

     <Tordernumbergenerator Uidpk="1" NextOrderNumber="${ep_initial_order_number}" />
1 Comments Permalink

PMD contains a useful set of default rules for enforcing best practices in Java code, but in some circumstances it may be useful to create additional PMD rules for best practices that are specific to your company's internal coding standards, a particular Java framework (such as Spring, OpenJPA, DROOLS, etc), or specifics of the Elastic Path code-base. Capturing these best-practices in PMD rules is often more effective than simply posting a list of standards on a wiki, which can easily be ignored or forgotten. Adding custom PMD rules is relatively simple, but not immediately obvious. In this blog post, I will outline the steps for adding custom PMD rules to your Elastic Path project and provide a reference for PMD attributes which do not appear to be documented anywhere online.

Defining an XPath Expression for the Rule

There are two ways to create custom PMD rules:

  • Write a rule using Java
  • Write an XPath expression

 

XPath expressions are much quicker to create, and are flexible enough to identify most conditions. For the purposes of this blog post, I will only focus on XPath expressions. For details about creating rules using Java classes, read PMD - How to Write a Rule.

 

The best way to get started is to write a simple class or interface that violates the rule that you chose.  For example, best practices state that Spring setters should be public on the class but not exposed on the interface. So I will create a rule to identify Spring setters that are exposed on an interface. Here is my simple example:

public interface CustomerAuthenticationService {
    void setElasticPath(final ElasticPath elasticPath);
    void setStoreProductService(final StoreProductService storeProductService);
}

First, launch the PMD Rule Designer. This can be run from the command line by executing designer.bat (or designer.sh in Linux) from <PMD_ROOT>/bin.  Alternatively, this tool can be launched from Eclipse (if PMD for Eclipse is installed) by opening Window --> Preferences, then PMD --> Rules Configuration, and clicking the Rule Designer button. Paste your example code into the "Source Code" text box, and then click the Go button below the "XPath Query" text box. The designer should then show an Abstract Syntax Tree as shown in the following screenshot.

 

PMD Rule Designer 1.png

 

We can use the Abstract Syntax Tree (AST) to help us create an XPath expression to identify the problem code. Each node in the AST represents an element of the Java Syntactical Grammar representing the example code we specified.  The XPath expression should navigate through the grammar hierarchy and select elements that violate your chosen rule. PMD also supports the use of XPath attributes (in square brackets) to further filter specific grammar elements based on name, access modifiers, class types, level of nesting, existence of comments, etc. Unfortunately, these attributes are not listed in the Rule Designer or documented anywhere on the PMD site. At the end of this article, I've listed some of the more useful PMD attributes (pulled from existing PMD rules).

 

If your XPath syntax skills are a bit rusty, Microsoft has an excellent tutorial available. There is also a simple XPath Rule Tutorial available on the PMD site. You may also find it useful to review the XPath expressions for the default PMD rules in  <PROJECT_ROOT>/ant/target/pmd/pmd-elasticpath-rules.xml to get a better feel for available XPath grammar nodes and attributes.

 

Here is the XPath expression that I came up with for this rule:

//ClassOrInterfaceDeclaration[@Interface='true']/ClassOrInterfaceBody/ClassOrInterfaceBodyDeclaration/MethodDeclaration/MethodDeclarator[starts-with(@Image, 'set') and (ends-with(@Image, 'Service') or ends-with(@Image, 'ElasticPath') or ends-with(@Image, 'Helper') or ends-with(@Image, 'Utility') or ends-with(@Image, 'Engine'))

 

To test the XPath expression in the Rule Designer, simply paste the expression into the "XPath Query" field, and click Go again.  We know our XPath expression works because the field to the right of the Abstract Syntax Tree indicates the position of two matches from our example source code (one for each setter method). Notice that the XPath expression closely matches the node heirarchy in the Abstract Syntax Tree.

Adding the Rule to the Ruleset File

Now that we have created the XPath expression for our rule, we need to add it to our project's ruleset file. Open <PROJECT_ROOT>/ant/target/pmd/pmd-elasticpath-rules.xml in a text editor and add the following immediately before the terminating </ruleset> tag:

<rule name="SpringInjectedSetterInInterface" 
      message="Spring-injected setters should not be declared in interface." 
      class="net.sourceforge.pmd.rules.XPathRule" dfa="false" externalInfoUrl="" typeResolution="true">
    <description>Spring-injected setters should not be declared in interface.</description>
    <priority>3</priority>
    <properties>
        <property name="xpath">
            <value><![CDATA[//ClassOrInterfaceDeclaration[@Interface='true']/ClassOrInterfaceBody/ClassOrInterfaceBodyDeclaration/MethodDeclaration/MethodDeclarator[starts-with(@Image, 'set') and (ends-with(@Image, 'Service') or ends-with(@Image, 'ElasticPath') or ends-with(@Image, 'Helper') or ends-with(@Image, 'Utility') or ends-with(@Image, 'Engine'))]]]></value>
        </property>
    </properties>
    <example><![CDATA[
public interface CustomerAuthenticationService {
     void setElasticPath(final ElasticPath elasticPath);
     void setStoreProductService(final StoreProductService storeProductService);
}
    ]]></example>
</rule>

The rule tag contains the following attributes:

  • name: The rule name which will be used for suppression annotations and for identifying the rule in build-stats.
  • message: The friendly rule description which will be displayed when the rule is violated.
  • class: For XPath-based rules, this should always be set to "net.sourceforge.pmd.rules.XPathRule"
  • dfa: Data Flow Analysis - this should always be set to "false" for XPath rules.
  • externalInfoUrl: If there is a web page that provides additional information about the violation, provide the link here.
  • typeResolution:This should always be set to "false" for XPath rules.

 

The rule tag also contains the following inner tags:

  • description: This should be the same as the rule tag message attribute value.
  • priority: An integer value in the range 1 to 5:
    • 1 = Error High - Change absolutely required. Behavior is critically broken/buggy.
    • 2 = Error - Change highly recommended. Behavior is quite likely to be broken/buggy.
    • 3 = Warning High - Change recommended. Behavior is confusing, perhaps buggy, and/or against standards/best practices.
    • 4 = Warning - Change optional. Behavior is not likely to be buggy, but more just flies in the face of standards/style/good taste.
    • 5 = Information - Change highly optional. Nice to have, such as a consistent naming policy for package/class/fields.
  • properties: This should contain a single <property> tag with name="xpath", containing a <value> tag with the XPath expression.
  • example: A simple example of the rule being violated.  It is often best to simply use the class or interface that you created for the Rule Designer when designing your XPath expression.

Making the Rule Available to Ant and PMD for Eclipse

Once the ruleset file has been updated, we can immediately run PMD from the command line by running ant pmd-all from the project root, or ant pmd from a specific project folder.

 

To detect the new rule from Eclipse using the PMD-Eclipse plugin, follow these steps:

  1. From the root of the EP source directory, run ant eclipse-setup-all.
  2. Refresh all projects in Eclipse.
  3. Right-click on the project and click PMD->Check Code with PMD.
  4. Wait for "Review Code" task to complete.

 

The new rule violations should now appear in your "Problems" view:


PMD_Problems_1.png

Custom PMD rules are an effective way to automatically enforce coding standards and best practices for all developers in a project. Since rule checking is automatic, developers don't need to know all of the standards by heart; they can learn the rules by breaking them and getting immediate feedback. This helps to train developers to create code that is more consistent and readable by all members of the team. There are even circumstances where PMD can identify bugs that wouldn't get caught by the Java compiler.

 

For example, a team I worked on was responsible for continuous improvement on a project that involved extensive use of integration tests to validate core functionality. Most of the integration classes extended an abstract class that provided helper methods and a constructor for setting up the initial database. Unfortunately, some of the test classes defined their own constructors that would re-initialize the database, even though the abstract class had already done the initialization. Although this didn't cause any actual problems for the tests, it significantly slowed down execution of the tests. A PMD rule was written to identify these circumstances. By creating this rule, we were able to not only validate that all of the test classes were fixed, but we were also able to ensure that future developers didn't make the same mistake.

 

Try to identify common pitfalls for your own team's developers, and create custom PMD rules to flag such patterns for future developers. The following attributes are grouped based on which node type they are available for. This is not an exhaustive list, but it represents the more commonly-used attributes.

 

  • All 
    • @Image (string) - Class/method/variable name
  • ClassOrInterfaceDeclaration 
    • @Interface (boolean) - Is an interface
    • @Class (boolean) - Is a class
    • @Nested (boolean) - Is a nested class (or interface)
    • @Abstract (boolean) - Is an abstract class
  • Block 
    • @ContainsComment (boolean) - Contains a comment
  • Initializer 
    • @Static (boolean) - Is declared as static
    • @Final (boolean) - Is declared as final
  • IfStatement 
    • @Else (boolean) - Is else portion of an if statement
  • SwitchStatement 
    • @Default (boolean) - Is default section of a switch statement
  • AllocationExpression 
    • @ArgumentCount (int) - Number of arguments in expression
  • FieldDeclaration / ConstructorDeclaration / MethodDeclaration 
    • @Public(boolean) - Is declared as public
    • @Protected (boolean) - Is declared as protected
    • @Private (boolean) - Is declared as private
    • @Static (boolean) - Is declared as static
    • @Synchronized (boolean) - Is declared as synchronized
  • PrimaryExpression 
    • @ArrayDereference (boolean)
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

Short screencast illustrating how to enable and use the Step Filtering feature of Eclipse:

 

 

Here are the step filters that should be enabled to skip over Spring's dynamic proxy code:

  • $Proxy*
  • java.*
  • javax.*
  • org.apache.*
  • org.springframework.*
  • sun.*
  • java.lang.ClassLoader
1 Comments Permalink

Anyone who has had the pleasure of customizing the Commerce Manager knows that it is a complicated piece of software with a lot of moving parts. 

 

However, with the release of 6.2.1 combined with the new binary-based architecture and a number of clients going through an upgrade process, it has become even more complex with the introduction of patch fragments and a new approach to customizations.

 

For this blog post, I wanted to demonstrate a handy Eclipse plug-in that lets you visualize the dependencies between the various plug-ins within the Commerce Manager.  This is really useful for quickly discovering which plug-ins/bundles are dependent on each other.

 

The plug-in is called "Plug-In Dependency Graph Plugin". You can download it from http://testdrivenguy.blogspot.com/2009/05/eclipse-plug-in-dependency-graph.html. Then just unpack it into your Eclipse plugins folder and restart Eclipse.

 

Open the "Graph Plug-in Dependencies" view, click the search icon in the top right corner, and type in the name of the plugin you want to check out. In this example, I am interested in the our customized admin.configuration plugin.

 

admin-config-callees.png

 

You can see that there are a lot of dependencies between all the plugins, but by highlighting the admin.configuration plugin I can quickly see which plugins are directly referenced by it.

 

Alternatively, if you want to see who is directly referencing your plugin, you can show the callers:

 

admin-config-callers.png

In this case, not too interesting.  Let's take a look at com.elasticpath.cmclient.core:

 

cmclient-core-callers.png

 

A little more interesting. This plugin can help you quickly track down configuration issues in your plug-in manifest files when developing using the new patch fragment approach.

 

In future posts, I will provide more technical details as to our approach in implementing the patch fragment architecture including technical challenges encountered and our solutions to these issues.

0 Comments Permalink

Out of the box, Elastic Path is configured to serve pages for the storefront. Some customers, however, need more flexibility. For example, we have a customer with content in a Content Management System (CMS) that needed to include links to EP content. The approach taken was to make Elastic Path content available as widgets which could be loaded remotely using JavaScript. This approach has the limitation that if the end user has JavaScript turned off then they will not be able to access the shopping functionality. The jQuery JavaScript library was used.

 

Product Browsing

To enable product browsing, the ShoppingItemConfigController was customized to support two request parameters: type and  productWidgetId. When the type parameter was set to "widget", the  ShoppingItemConfigController returned the widget product view. This  product view was customised to fit in the small space allowed on the  page and a custom template was created for this. The productWidgetId parameter was used to identify the widget so that  multiple widgets could exist on one page.

 

The host page from the CMS included a div at the required location for the product widget:

<div class="blockElement" id="product-widget-1"></div>

 

A script element on the page included the following code:

$(document).ready(function() {
    $('#product-widget-1').load('storefront/category1/productCode.html?type=widget&productWidgetId=product-widget-1');
}

This code executes after the page loads but before all sub-content (e.g. images)  is loaded, sending an asynchronous request to EP for the product content and loading it in  the div with the id "product-widget-1".

 

Add to Cart

To add an item to the cart, an asynchronous call to DWR was made using the widget id to identify the SKU and quantity to add.

<form id="skuSelectForm" name="skuSelectForm" method="post" onsubmit="return addToCartSubmit('#$productWidgetId'); ">

 

function addToCartSubmit(widgetId) {      
    shoppingCartAjaxController.addSkuToCart(jQuery(widgetId + ' 
        .skuCodeParameter').val(), jQuery(widgetId +'-quantitySelect').val(), 
        function(data) {
            refreshShoppingCartSummary();
       
        });
   return false;
}


Shopping Cart Summary

When the addSkuToCart request returns, the refreshShoppingCartSummary() method is called. This is defined in a JavaScript file included in the hosted page. It makes another asynchronous call in order to determine the number of items in the cart. (Note: for greater network efficiency, the number of items in the cart could be returned from addSkuToCart.)

function refreshShoppingCartSummary() {
    shoppingCartAjaxController.getCartItemCount(function(data) { 
        jQuery('#cartSummaryText').text(data); 
    });            
};

 

Shopping Cart Summary Popup

The jQuery hover method was used  to detect when the mouse was over the icon and display a popup showing the contents of the cart. A DWR call was made to retrieve the cart data. To reduce the number of round trips, this call was designed to return all required information for the cart and its items. JavaScript was used to build the DOM tree for this data. (Note that, in production, a JS client side templating engine should be considered for this role.)

 

The next issue was making sure the popup remained open when the mouse moved to the checkout button. This was done using JavaScript timeouts; when the hover out function was called, the timeout would start. When a hover in function was called for the cart icon or for the popup, the timeout would be cancelled. If the timeout fired without being cancelled, the popup would be closed.

 

    $('#shoppingCartMenu').hover(function() {
        shoppingCartAjaxController.getCartSummary(function(cart) {
            var popupHtml = '<div class="cartTop">';           
            popupHtml += '</div>';
            popupHtml += '<div class="cartBottom transparent">';
            popupHtml +=    '<div class="yourCart">';
            popupHtml +=        '<p>YOUR CART</p>';
            popupHtml +=    '</div>'

            for (var i=0; i<cart.shoppingItems.length; i++) {
                popupHtml += '<div class="product">';
                shoppingItem = cart.shoppingItems[i];
                popupHtml +=    '<div class="items">';
                popupHtml +=        '<div class="itemsImg">';
                popupHtml +=            '<img width="35" height="41" border="0" alt="' + shoppingItem.localizedProductName;
                popupHtml += '" src="/storefront/renderImage.image?imageName=' + shoppingItem.imageFileName +'&width=35&height=41" id="productImage">';
                popupHtml +=        '</div>';
                popupHtml +=        '<div class="itemsText">';
                popupHtml +=            '<p><strong>' + shoppingItem.localizedProductName + '</strong></p>';
                popupHtml += '<p>';
                for (var j=0; j<shoppingItem.localizedSkuOptionDisplayNames.length; j++) {
                    if (j != 0) popupHtml += ',';
                    popupHtml += shoppingItem.localizedSkuOptionDisplayNames[j];
                }
                popupHtml += '</p>';
                popupHtml +=        '</div>';                                               
                popupHtml +=    '</div>';   
                popupHtml +=    '<div class="priceSubtotal">';
                popupHtml +=        '<p>Qty: ' + shoppingItem.quantity +'</p>';
                popupHtml +=    '</div>';
                popupHtml +=    '<div class="price">';
                popupHtml +=        '<h2><p>' + getCents(shoppingItem.total) + '</p><h2>' + getDollars(shoppingItem.total) + '</h2><p>$</p></h2>';
                popupHtml +=    '</div>';
                popupHtml += '</div>';
            }
            popupHtml += '<div class="subtotal">';
            popupHtml +=    '<div class="items">';
            popupHtml +=        '<p>ITEMS IN CART: ' + cart.numItems + '</p>';
            popupHtml +=    '</div>';   
            popupHtml +=    '<div class="priceSubtotal">';
            popupHtml +=        '<p>SUBTOTAL:</p>';
            popupHtml +=    '</div>';
            popupHtml +=    '<div class="price">';

            popupHtml +=        '<h2><p>' + getCents(cart.subTotal) + '</p><h2>' + getDollars(cart.subTotal) + '</h2><p>$</p></h2>';
            popupHtml +=    '</div>';                                               
            popupHtml += '</div>';                                   
            popupHtml += '<div class="checkout">';
            popupHtml +=    '<a href="/storefront/check-out.ep" title=""><img src="images/btn_checkout.gif" alt="" /></a>';
            popupHtml += '</div>';
            popupHtml += '</div>';
            $('#shoppingCartPopup').html(popupHtml);
            $('#shoppingCartPopup').css('display', 'block');
        });
    }, function() {
        shoppingCartPopupClose = setTimeout(closeShoppingCartPopup();, 500);
    });
   
    $('#shoppingCartPopup').hover(function() {
        clearTimeout(shoppingCartPopupClose);
    }, function() {
        shoppingCartPopupClose = setTimeout(closeShoppingCartPopup();, 500);
    })
1 Comments Permalink

Actions