Technical Blog

12 Posts tagged with the customization tag

Having fun with SWT

Posted by Arman Sharif Sep 9, 2011
Background

Being new to the SWT I wanted to learn a little about the API by making a simple change to the Commerce Manager UI. I put this post together with the hope that it might have some useful tidbits for other SWT newbies out there.

 

Goal

The CM client displays a product icon in the catalog's Product Listing view. The icon may vary depending on whether the product is a bundle or has multiple SKUs, but otherwise it is the same icon for every product. To make things more interesting I decided to customise the view to generate an icon specific to each individual product. In the real world, the image location would probably come from a product attribute. To keep things simple however we will simply generate a small square image with a random background colour.

 

 

Solution

 

To achieve our goal we will create a new class ProductIconImageRegistry. This class will be responsible for creating SWT product image icons that can be obtained via the following get(Product) method.

 

/**
* Returns an image for the given product.
*/
public Image get(final Product product) {
     final Long key = product.getUidPk();
     Image image = imageCache.get(key);
 
     if (image == null) {
          if (isCacheFull()) {
               freeCache();
          }
               
          image = getProductIcon(product);
          addToCache(key, image);
     }
          
     return image;
}

 

 

Because every new SWT image instance requires an allocation of OS resources, we will cache the icons in a hash map. To keep the cache from growing indefinitely we will define an upper limit and free up some cache when it reaches the maximum size.

 

Two important rules to bear in mind (see References) when working with SWT components are:

 

    "If you created it, you dispose it."

    "Disposing the parent disposes the children"

 

The rules apply to a number of SWT classes including Image, Color, Font, Widget, GC, and so forth. If you invoke a constructor to instantiate one of these classes, you must free them using the dispose() method.

 

Color color = new Color(...); // allocates platform resources
color.dispose();

 

However if you acquire an instance without calling the constructor there is no need to dispose().

 

Color color = display.getSystemColor(SWT.COLOR_BLUE);

 

In the case of the ProductIconImageRegistry class, we are creating a new icon image as follows:

 

/**
  * Returns an icon for the given product.
  */
private Image getProductIcon(final Product product) {
     final Display display = Display.getCurrent();
     final Color color = createRandomColor(display);
     final Image iconImage = createIconImage(display, color);
     return iconImage;
}
 
/**
  * Creates a random Color.
  */
private Color createRandomColor(final Display display) {
     final int red = random.nextInt(256);
     final int green = random.nextInt(256);
     final int blue = random.nextInt(256);
     return new Color(display, red, green, blue);
}
 
/**
  * Creates an Image with the specified background Color.
  */
private Image createIconImage(final Display display, final Color color) {
     final Image image = new Image(display, ICON_LENGTH, ICON_LENGTH);
     final GC gc = new GC(image);
     gc.setBackground(color);
     gc.fillRectangle(0, 0, ICON_LENGTH, ICON_LENGTH);
     
     drawIconBorder(gc, display.getSystemColor(SWT.COLOR_GRAY));
     gc.dispose();
     return image;
}
 
/**
  * Draws a border around the icon.
  */
private void drawIconBorder(final GC gc, final Color borderColor) {
     final int border = ICON_LENGTH - 1;
     gc.setForeground(borderColor);
     gc.drawLine(0, 0, 0, border);
     gc.drawLine(0, 0, border, 0);
     gc.drawLine(0, border, border, border);
     gc.drawLine(border, 0, border, border);
}

 

 

Our freeCache() method will simply purge a tenth of its contents and call dispose() on every removed image.

 

private void freeCache() {
     int removeQty = CACHE_SIZE / 10;
     for (int i = 0; i < removeQty; i++) {
          final Long removedKey = cacheKeys.removeFirst();
          final Image removedImage = imageCache.remove(removedKey);
          removedImage.dispose();
     }
}

 

We will also provide a disposeAllImages() method on the ProductIconImageRegistry class to allow the client code purge everything on shutdown.

 

public void disposeAllImages() {
     for (Long key : imageCache.keySet()) {
          final Image image = imageCache.get(key);
          image.dispose();
     }
     imageCache.clear();
     cacheKeys.clear();
}
 

 

Finally, we need to plug in our new class into the existing CatalogImageRegistry and CoreImageRegistry classes. This can be done by updating the getImageForProduct(Product) method:

 

public static Image getImageForProduct(final Product product) {
     if (product instanceof ProductBundle) {
          return getImage(PRODUCT_BUNDLE);
     }
          
     if (product.hasMultipleSkus()) {
          return getImage(PRODUCT_MULTI_SKU);
     }
          
     return getImage(PRODUCT);
}

 

and replacing the last line with

 

return productIconImageRegistry.get(product);

 

We also need to hook in the disposeAllImages() method:

 

static void disposeAllImages() {
     for (final ImageDescriptor desc : IMAGES_MAP.keySet()) {
          final Image image = IMAGES_MAP.get(desc);
          if (!image.isDisposed()) {
               image.dispose();
          }
     }
 
     productIconImageRegistry.disposeAllImages();
}

 

With the all pieces together, the final result looks as shown in the screen shot. The icons in the Product Listing view are provided by the CatalogImageRegistry class.

 

grep-cmclient.png

 

The CoreImageRegistry class provides icons for the Select a Product dialog, which looks as follows with the changes in place:

 

grep-cmclient-search.png

 

 

 

References

 

http://www.eclipse.org/articles/Article-SWT-images/graphics-resources.html

http://www.eclipse.org/articles/swt-design-2/swt-design-2.html

http://eclipse.org/articles/Article-SWT-graphics/SWT_graphics.html

0 Comments Permalink

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

Hello all,

 

In a recent project we began using the @Autowired spring annotation in our objects and services to help simplify and minimize the amount of configuration we were writing in our various xml files.

 

Introduced in Spring 2.5 annotation-based configurations are now a viable alternative to pure XML configuration.  While it is debatable if one approach is “better” than the other, in practice it seems that mixing both methods allows you to spend less time wiring everything up and just getting on with it.

 

There are a number of annotations available including support for JSR-250 and JSR-330 but for this article I will be focusing purely on the @Autowired annotation.  For more information please refer to the Spring documentation or any number of articles on the web.

 

What’s the point?

 

How many times have you had to explicitly wire your spring beans and services hunting for the proper bean names?  What about creating setter methods for your private objects?  What if there were an easier way to do it?

 

Spring 2.5 (and beyond) has introduced a new xml context schema that deals with ApplicationContext configuration related to plumbing – that is, not usually beans that are important to an end-user but rather beans that do a lot of the grunt work in Spring, such as BeanFactoryPostProcessors.  To enable the tags in the context namespace, simply add the context namespace to your schema definition:

 

 

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

<!-- <bean/> definitions here -->

</beans>

 

 

Now let’s look at a class as an example to see how @Autowired can help you in your daily development.

 

package emptest;
public interface EmployeeService {
         String hire(String name);
         String fire(String name);
}
 
 
package emptest.impl;
@Service
public class EmployeeServiceImpl implements EmployeeService {
 
         private EmployeeDao employeeDao;
 
         public String hire(String name) {
                 String message = employeeDao.getMessage("Hire");
                 return name + ", " + message;
         }
 
         public String fire(String name) {
                 String message = employeeDao.getMessage("Fire");
                 return name + ", " + message;
         }
 
         public void setEmployeeDao(EmployeeDao employeeDao) {
                 this.employeeDao = employeeDao;
         }
}
 

 

In this simple example we have a service implementing an interface with a private DAO object.  In a typical spring configuration for this service typically you would define your bean as:

 

 

<bean id="employeeService" class="emptest.impl.EmployeeServiceImpl">
         <property name="employeeDao">
                 <ref bean=”employeeDao”/>
         </property>
</bean>

 

 

But Spring can automatically detect classes and register bean definitions with the ApplicationContext!  Let’s see how this works in practice.

 

Step 1.  Add <context:component-scan base-package=”emptest.impl”/> to your configuration file.  This tells spring to scan everything in the emptest.impl package for Spring stereotypes and annotations.

 

 

<beans xmlns="http://www.springframework.org/schema/beans"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"
     xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
     xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
                 http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

     <context:component-scan base-package="emptest.impl"/>


</beans>

 

 

Step 2.  Add @Autowired annotation to any private objects that you would like Spring to wire up for you.

 

package emptest.impl;
@Service
public class EmployeeServiceImpl implements EmployeeService {
         @Autowired
         private EmployeeDao employeeDao;
 
         public String hire(String name) {
                 String message = employeeDao.getMessage("Hire");
                 return name + ", " + message;
         }
 
         public String fire(String name) {
                 String message = employeeDao.getMessage("Fire");
                 return name + ", " + message;
         }        
}
 
 

 

That’s it!  Notice that there is no need to explicitly define a setter method, nor do you need to explicitly define your bean within the Spring configuration.

 

Of course this is a very basic scenario, but for the most part will get you started using annotation based configuration.  There are lots of tutorials, guides and documentation available online for more advanced usages.

 

Cheers!

1 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

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

Every order in Elastic Path Commerce must have a unique number. Out of the box, we use an auto-incrementing value stored in the TORDERNUMBERGENERATOR table. Here is the OpenJPA annotation for the orderNumber property in OrderImpl.java:

      @Basic
      @Column(name = "ORDER_NUMBER", length = GUID_LENGTH, nullable = false, unique = true)
      @GeneratedValue(strategy = GenerationType.TABLE, generator = NEXT_ORDER_NUMBER)
      @TableGenerator(name = NEXT_ORDER_NUMBER, table = "TORDERNUMBERGENERATOR", pkColumnName = "UIDPK",
                 valueColumnName = NEXT_ORDER_NUMBER, pkColumnValue = "1", allocationSize = 1)
      public String getOrderNumber() {
            return this.orderNumber;
      }

The first annotation declares orderNumber as a basic property. The second annotation declares ORDER_NUMBER as the column to save/load the property. The third annotation declares this as an auto-generated value, so no need to explicitly assign a value when the order is created in our code. The fourth annotation says that we are using the table generator. The next available order number is retrieved from the NEXT_ORDER_NUMBER column in TORDERNUMBERGENERATOR.

 

You may need to customize the number generation. For example, one of our customers has multiple data centers and each data center is running its own separate production deployment of Elastic Path Commerce. Order data from the different data centers is sent to a centralized financial application. The financial application needs to have unique order numbers, but each Elastic Path deployment uses the same order number generator algorithm, so there will be duplicate order numbers across the different data centers.

 

Having a centralized order number generator was not an option. The solution was to add a prefix to every order number on all data centers. The prefix would identify the data center where the order was created and ensure that order numbers would be unique across all data centers. To implement this, we would need to do the following:


  • Create a CUSTOMIZEDORDERNUMBERGENERATOR database table with columns (UIDPK number, PREPEND varchar 3, NEXT_ORDER_NUMBER varchar 100). Each data center would store a different prefix in the PREPEND column.
  • Create a CustomizedOrderNumberGenerator, which would read the PREPEND column and prepend it to  the NEXT_ORDER_NUMBER.
  • Override the annotations for OrderImpl.getOrderNumber to use the new custom generator.


OpenJPA represents all generators internally with the org.apache.openjpa.kernel.Seq interface. This interface supplies all the context we need to create  our own custom generators, including the current persistence  environment, the JDBC DataSource,  and other essentials. The org.apache.openjpa.jdbc.kernel.AbstractJDBCSeq helps us create custom JDBC-based sequences. There are many  implementations in OpenJPA to generate different values as well, like AbstractJDBCSeq, ClassTableJDBCSeq, DelegatingSeq, NativeJDBCSeq, TableJDBCSeq, TimeSeededSeq, UUIDHexSeq, UUIDStringSeq, UUIDType4HexSeq, UUIDType4StringSeq, ValueTableJDBCSeq. Here is our CustomizedOrderNumberGenerator:

public class CustomizedOrderNumberGenerator extends ValueTableJDBCSeq {
 
    private static final String TABLE_NAME = "CUSTOMIZEDORDERNUMBERGENERATOR";
    private static final String PRIMARY_KEY_COLUMN = "UIDPK";
    private static final String PRIMARY_KEY_VALUE = "1";
    private static final String SEQUENCE_COLUMN = "NEXT_ORDER_NUMBER";
    private static final String PREFIX_COLUMN = "PREPEND";
 
    protected Object currentInternal(final JDBCStore store, final ClassMapping mapping) throws Exception {        
        return getPrefix(store, mapping) + super.currentInternal(store, mapping);    
    }
    
    protected Object nextInternal(final JDBCStore store, final ClassMapping mapping) throws Exception {
        return getPrefix(store, mapping) + super.nextInternal(store, mapping);    
    }
    
    protected String getPrefix(final JDBCStore store, final ClassMapping mapping) throws EpSystemException, SQLException {    
        if (prefix == null) {
            // Load from db
            Object primaryKey = getPrimaryKey(mapping);
    
            if (primaryKey == null) {    
                LOG.error(ERROR_CANNOT_GET_ORDER_PREFIX);
                throw new EpSystemException(ERROR_CANNOT_GET_ORDER_PREFIX);            
            }
            
            Connection conn = getConnection(store);            
            try {            
                prefix = getPrefix(conn);            
            } finally {            
                closeConnection(conn);            
            }         
        }            
        return prefix;            
    }
        
    
    private String getPrefix(final Connection conn) throws EpSystemException, SQLException {
        // Prepare the statement
        DBDictionary dict = getConfiguration().getDBDictionaryInstance();
        SQLBuffer sel = new SQLBuffer(dict).append(PREFIX_COLUMN);
        SQLBuffer where = new SQLBuffer(dict).append(PRIMARY_KEY_COLUMN).append(" = ").append(PRIMARY_KEY_VALUE);
        SQLBuffer tables = new SQLBuffer(dict).append(TABLE_NAME);
        
        SQLBuffer select = dict.toSelect(sel, null, tables, where, null, null,
                null, false, false, 0, Long.MAX_VALUE, false, true);
 
        PreparedStatement stmnt = select.prepareStatement(conn);
        
        ResultSet resultSet = null;
        
        try {
            //Do query
            resultSet = stmnt.executeQuery();
 
            if (!resultSet.next()) {
                LOG.error(ERROR_CANNOT_GET_ORDER_PREFIX);
                throw new EpSystemException(ERROR_CANNOT_GET_ORDER_PREFIX);
            }
            return dict.getString(resultSet, 1);
    
        } finally {        
            ...        
        }   
    }
}

 

The last step is to overwrite  the generator in order-orm.xml:

    <entity class="OrderImpl">
        <sequence-generator name="NEXT_ORDER_NUMBER" sequence-name="com.customize.CustomizedOrderNumberGenerator()" allocation-size="1" />
       ...
   </entity>

 

For more information on table generators, see the following links:

http://openjpa.apache.org/builds/1.2.0/apache-openjpa-1.2.0/docs/manual/jpa_overview_mapping_sequence.html#jpa_overview_mapping_sequence_tablegen

http://openjpa.apache.org/builds/1.0.2/apache-openjpa-1.0.2/docs/manual/jpa_overview_mapping_sequence.html

http://openjpa.apache.org/builds/1.0.2/apache-openjpa-1.0.2/docs/manual/ref_guide_sequence.html

1 Comments Permalink

Targeted Selling is Elastic Path's personalization engine. It collects data about the shopper from various sources and exposes it for rule-based conditional evaluation. In Elastic Path, this is done via Business User rule configurations. The underlying technology put in place to support Targeted Selling is called the Tagging Framework, enabling the system to perform tag-based customer segmentation.

 

The following diagram shows how the building blocks fit together and form the basis of the various areas of the Storefront that is exposed to a customer for interaction. As you can see, price lists and dynamic content are both driven by these two frameworks.

Architecture Overview.jpg

Be sure to check out the Tagging Framework and Targeted Selling documentation on the Elastic Path documentation site for details. For our purposes, today's blog entry will consist of an example of how we can customize and extend these frameworks to provide rule-driven personalization for a straightforward use case.

 

Aa a Sales Engineer, I frequently get asked to personalize our storefront presentation to demonstrate some scenarios that are specific to a prospective client. Here are two recent examples where I needed to set up the system to be configurable and present specific dynamic content for two different customer personas:

  • Education Pricing: the system should be able to provide education-level pricing that differs according to which school a customer belongs to
  • Membership Levels: the system should be able to provide personalized merchandising and pricing to customers at Bronze, Silver and Gold tiers, as well as Staff or Affiliate pricing

 

Now, I'm quite lazy and would be quite happy to spend my day browsing The Daily WTF. I definitely do not want to start writing new code every time the business/marketing admins want to start segmenting customers via new profile attributes. Therefore, I want this customization to the system to be easily configurable. In this post, we'll look at how I did it.

 

The steps we'll be running through include:

  • Setting up the new customer profile and tagging data to support the customer segmentation
  • Setting up segment specific pricing and dynamic content rules
  • Extending the system to add a configurable Tagger in the storefront to perform the segmentation
  • Testing the new configurable Tagger
  • Tying everything together with Spring

 

Setting Up the Underlying Tag Data
Adding Customer Profile Attributes

I've elected to add two new String-based customer profile attributes via SQL rather than the Commerce Manager client. In this case, we're looking at two custom attributes that are automatically available for editing in the Commerce Manager's Customer editor, as seen below. Optionally, we could have made these attributes System attributes and added custom drop-down boxes with pre-defined values for valid membership levels and schools respectively for the Customer editor. I'll leave that as an optional exercise.

INSERT INTO TATTRIBUTE (UIDPK,ATTRIBUTE_KEY,LOCALE_DEPENDANT,ATTRIBUTE_TYPE,NAME,REQUIRED,VALUE_LOOKUP_ENABLED,ATTRIBUTE_USAGE,SYSTEM,ATTR_GLOBAL)
    VALUES (1000,'CP_MEMBERSHIP',0,1,'Membership Level',0,0,4,0,0);

INSERT INTO TATTRIBUTE (UIDPK,ATTRIBUTE_KEY,LOCALE_DEPENDANT,ATTRIBUTE_TYPE,NAME,REQUIRED,VALUE_LOOKUP_ENABLED,ATTRIBUTE_USAGE,SYSTEM,ATTR_GLOBAL)
    VALUES (1001,'CP_SCHOOL',0,1,'School',0,0,4,0,0);

screen-capture-3.png

 

Adding Custom Tag Definitions and Values

Next we're going to make the Tagging framework aware of new tags for Membership and Schools. We're also restricting the possible allowed values for each respective tag.

INSERT INTO TTAGVALUETYPE(uidpk, guid, java_type) values(20,'school','java.lang.String');

INSERT INTO TTAGVALUETYPE(uidpk, guid, java_type) values(21,'membership','java.lang.String');

INSERT INTO TTAGVALUETYPEOPERATOR(tagvaluetype_guid, tagoperator_guid)
    values('school', 'equalTo'), ('school', 'notEqualTo');

INSERT INTO TTAGVALUETYPEOPERATOR(tagvaluetype_guid, tagoperator_guid)
    values('membership', 'equalTo'), ('membership', 'notEqualTo');

INSERT INTO TTAGDEFINITION(uidpk, guid, name, description, tagvaluetype_guid, taggroup_uid)
    values(30, 'school', 'school', 'school', 'school', 3);

INSERT INTO TTAGDEFINITION(uidpk, guid, name, description, tagvaluetype_guid, taggroup_uid)
    values(31, 'membership', 'membership', 'membership', 'membership', 3);

insert into TTAGALLOWEDVALUE(uidpk, value, tagvaluetype_guid, description, ordering)
     values(42, 'gold', 'membership', 'Gold', 1),
     (43, 'staff', 'membership', 'Staff', 2),
     (44, 'affiliate', 'membership', 'Affiliates', 3);

insert into TTAGALLOWEDVALUE(uidpk, value, tagvaluetype_guid, description, ordering)
     values(45, 'sfu', 'school', 'Simon Fraser University', 1),
     (46, 'ubc', 'school', 'University of British Columbia', 2),
     (47, 'ua', 'school', 'University of Alberta', 3);

 

Adding Localized Content

For completeness, we're adding localized content so that our Pricelist Assignment and Dynamic Content Delivery rule editors will display our new tags appropriately for administrators.

INSERT INTO TLOCALIZEDPROPERTIES (UIDPK,LOCALIZED_PROPERTY_KEY,VALUE,TYPE,OBJECT_UID)
    values (200000,'tagDefinitionDisplayName_en','is from a school','TagDefinition',30);

INSERT INTO TLOCALIZEDPROPERTIES (UIDPK,LOCALIZED_PROPERTY_KEY,VALUE,TYPE,OBJECT_UID)
    values (200001,'tagDefinitionDisplayName_en','has a membership','TagDefinition',31);

INSERT INTO TLOCALIZEDPROPERTIES (UIDPK,LOCALIZED_PROPERTY_KEY,VALUE,TYPE,OBJECT_UID)
    values (200002,'tagDefinitionDisplayName_fr','is from a school','TagDefinition',30);

INSERT INTO TLOCALIZEDPROPERTIES (UIDPK,LOCALIZED_PROPERTY_KEY,VALUE,TYPE,OBJECT_UID)
    values (200003,'tagDefinitionDisplayName_fr','has a membership','TagDefinition',31);

 

Adding Tags to the Tag Dictionaries

The system groups tags into tag dictionaries, such as those for pricing and shopper segmentation. We're adding our new tags into these dictionaries so that they show up in the UI for configuration.

INSERT INTO TTAGDICTIONARYTAGDEFINITION(tagdictionary_guid, tagdefinition_guid)
    values('SHOPPER', 'school');

INSERT INTO TTAGDICTIONARYTAGDEFINITION(tagdictionary_guid, tagdefinition_guid)
    values('PLA_SHOPPER', 'school');

INSERT INTO TTAGDICTIONARYTAGDEFINITION(tagdictionary_guid, tagdefinition_guid)
    values('SHOPPER', 'membership');

INSERT INTO TTAGDICTIONARYTAGDEFINITION(tagdictionary_guid, tagdefinition_guid)
    values('PLA_SHOPPER', 'membership');

 

Configuring the School Pricing

Once this data is all loaded, we can now load up our Price List Assignment editor or Dynamic Content Delivery editor and be able to configure rules based on the new Membership or School tags, such as below.

 

schools.png

 

As such, for a demo I've configured school level pricing for a specific camera to provide a price that is discounted to $150.00 from $199.00. However, this won't show up right away on the storefront because we need to have the School memberships added into a customer's tagset when they are interacting with the storefront.

screen-capture-5.png

 

Creating our own Tagger

To enable the storefront to be aware of the new tags, we're extending the existing CustomerProfileTagger. We're making the custom tagger extensible to automatically add in any configured customer profile attributes into the tagset. Note that the CustomerProfileTagger implements two different listeners for login and session creation events, and thus is executed when these events are fired on the storefront.

package com.elasticpath.sfweb.listeners;
 
import java.util.Iterator;
import java.util.Map;
 
import javax.servlet.http.HttpServletRequest;
 
import org.apache.log4j.Logger;
 
import com.elasticpath.domain.customer.Customer;
import com.elasticpath.domain.customer.CustomerSession;
import com.elasticpath.tags.Tag;
import com.elasticpath.tags.TagSet;
 
/**
 * Applies configured list of tag values into tagset.
 * 
 * @author drewz
 *
 */
public class ConfigurableCustomerProfilerTagger extends CustomerProfileTagger {
 
     private static final Logger LOG = Logger.getLogger(ConfigurableCustomerProfilerTagger.class);
     
     private Map<String, String> profileAttributeTags;
 
     /**
      * Apply configured string-based tags into session's tagset.
      * 
      * @param session instance of CustomerSession
      * @param request the originating HttpServletRequest
      */
     public void execute(final CustomerSession session, final HttpServletRequest request) {
          super.execute(session, request);
          
          if (profileAttributeTags == null || profileAttributeTags.isEmpty()) {
               return;
          }
          
          TagSet tagCloud = session.getCustomerTagSet();
          Customer customer = session.getCustomer();
          
          Iterator<String> iter = profileAttributeTags.keySet().iterator();
          while (iter.hasNext()) {
               String attributeKey = iter.next();
               String tagKey = profileAttributeTags.get(attributeKey);
               String attrValue = customer.getCustomerProfile().getStringProfileValue(attributeKey);
               
               if (attrValue == null) {
                    LOG.debug("Customer attribute " + attributeKey + " not available. Tag value not added to tag cloud.");
               } else {
                    LOG.debug("Adding customer tag " + tagKey + " to tag cloud: " + attrValue);
                    tagCloud.addTag(tagKey, new Tag(attrValue));
               }
          }
     }
     
     public void setProfileAttributeTags(Map<String, String> profileAttributeTags) {
          this.profileAttributeTags = profileAttributeTags;
     }
}

 

Testing the Tagger

No coding is complete without valid test scenarios! In this case, we're testing that any configured profile attributes are added to the tagset correctly, and any missing/empty profile attributes are not added.

/**
 * 
 */
package com.elasticpath.sfweb.listeners;
 
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
 
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
 
import org.jmock.Expectations;
import org.jmock.Mockery;
import org.jmock.integration.junit4.JMock;
import org.jmock.integration.junit4.JUnit4Mockery;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.mock.web.MockHttpServletRequest;
 
import com.elasticpath.domain.customer.Customer;
import com.elasticpath.domain.customer.CustomerProfile;
import com.elasticpath.domain.customer.CustomerSession;
import com.elasticpath.tags.TagSet;
 
/**
 * Tests for CustomerProfileTagger.
 *
 */
@RunWith(JMock.class)
public class ConfigurableCustomerProfileTaggerTest {
 
     private static final String MEMBERSHIP_ATTR_KEY = "MEMBERSHIP";
     private static final String SCHOOL_ATTR_KEY = "SCHOOL";
     private static final String MEMBERSHIP_TAG_KEY = "membership";
     private static final String SCHOOL_TAG_KEY = "school";
 
     
     private final Mockery context = new JUnit4Mockery();
          
     private TagSet tagSet;
     private MockHttpServletRequest request;
     private CustomerSession session;
     private Customer customer;
     private CustomerProfile customerProfile;
     private ConfigurableCustomerProfilerTagger listener;
     
     /**
      * Setting up instances.
      */
     @Before
     public void setUp() {
          tagSet = new TagSet();
          request = new MockHttpServletRequest();
          session = context.mock(CustomerSession.class);
          customer = context.mock(Customer.class);
          customerProfile = context.mock(CustomerProfile.class);
          listener = new ConfigurableCustomerProfilerTagger();
     }
     
     /**
      * Test that configured list of profile attributes are added to the tagset. 
      * Testing two tags/attributes for membership and school.
      */
     @Test
     public void testAddExistingProfileAttributeToTagSet() {
          final String membershipValue = "GOLD";
          final String schoolValue = "SFU";
          
          context.checking(new Expectations() { {
               allowing(session).getCustomerTagSet(); will(returnValue(tagSet));
               allowing(session).getCustomer(); will(returnValue(customer));
               allowing(customer).getCustomerProfile(); will(returnValue(customerProfile));
               allowing(customer).getDateOfBirth(); will(returnValue(new Date()));
               allowing(customer).getGender(); will(returnValue('M'));
               allowing(customerProfile).getStringProfileValue(MEMBERSHIP_ATTR_KEY); will(returnValue(membershipValue));
               allowing(customerProfile).getStringProfileValue(SCHOOL_ATTR_KEY); will(returnValue(schoolValue));
          } });
          
          
          Map<String, String> profileAttributeTags = new HashMap<String, String>(2);
          profileAttributeTags.put(MEMBERSHIP_ATTR_KEY, MEMBERSHIP_TAG_KEY);
          profileAttributeTags.put(SCHOOL_ATTR_KEY, SCHOOL_TAG_KEY);
          listener.setProfileAttributeTags(profileAttributeTags);
          listener.execute(session, request);
          
          assertNotNull("Membership tag was null", tagSet.getTagValue(MEMBERSHIP_TAG_KEY));
          assertEquals("Failed to get the correct membership value from the tagset", 
                    membershipValue, tagSet.getTagValue(MEMBERSHIP_TAG_KEY).getValue());
          assertNotNull("School tag was null", tagSet.getTagValue(SCHOOL_TAG_KEY));
          assertEquals("Failed to get the correct membership value from the tagset", 
                    schoolValue, tagSet.getTagValue(SCHOOL_TAG_KEY).getValue());
     }
     
     /**
      * Test that if a customer profile attribute is missing for a customer, no tag value is added to the tagset.
      */
     @Test
     public void testMissingProfileAttributeToTagSet() {
 
          context.checking(new Expectations() { {
               allowing(session).getCustomerTagSet(); will(returnValue(tagSet));
               allowing(session).getCustomer(); will(returnValue(customer));
               allowing(customer).getCustomerProfile(); will(returnValue(customerProfile));
               allowing(customer).getDateOfBirth(); will(returnValue(new Date()));
               allowing(customer).getGender(); will(returnValue('M'));
               allowing(customerProfile).getStringProfileValue(MEMBERSHIP_ATTR_KEY); will(returnValue(null));
               allowing(customerProfile).getStringProfileValue(SCHOOL_ATTR_KEY); will(returnValue(null));
          } });
          
          
          Map<String, String> profileAttributeTags = new HashMap<String, String>(2);
          profileAttributeTags.put(MEMBERSHIP_ATTR_KEY, MEMBERSHIP_TAG_KEY);
          profileAttributeTags.put(SCHOOL_ATTR_KEY, SCHOOL_TAG_KEY);
          listener.setProfileAttributeTags(profileAttributeTags);
          listener.execute(session, request);
          
          assertEquals("Membership tag was not null", null, tagSet.getTagValue(MEMBERSHIP_TAG_KEY));
          assertEquals("School tag was not null", null, tagSet.getTagValue(SCHOOL_TAG_KEY));
     }
     
     
}

 

Tying it all Together

With our new tagger tested and verified, we now configure it into the system via the Storefront's serviceSF.xml spring configuration by replacing the default customerProfileTagger with our new custom tagger. Note that we're injecting a map of profile attributes that will be added to the tagset.

     <bean id="customerProfileTagger" class="com.elasticpath.sfweb.listeners.ConfigurableCustomerProfilerTagger" >
          <property name="profileAttributeTags">
               <map>
                    <entry key="CP_MEMBERSHIP" value="membership"/>
                    <entry key="CP_SCHOOL" value="school"/>
               </map>
          </property>
     </bean>

 

     Similarly, in the Commerce Manager application we need the UI to be able to handle the new tags, thus in serviceCM.xml we add in our value providers for the Tag Values.

     <bean id="selectableTagValueServiceLocator" class="com.elasticpath.tags.service.impl.SelectableTagValueServiceLocatorImpl">
          <property name="valueProviders">
               <map>
                              <!-- SNIPPED ... -->          
                    <entry><key><value>school</value></key>
                            <bean class="com.elasticpath.tags.service.impl.InternalSelectableStringTagValueProviderImpl"></bean>
                    </entry>
                    <entry><key><value>membership</value></key>
                            <bean class="com.elasticpath.tags.service.impl.InternalSelectableStringTagValueProviderImpl"></bean>
                    </entry>          
               </map>
          </property>
     </bean>

 

Tie this together and run the storefront with a test customer with a school membership, and suddenly we have the school level pricing we configured earlier. Custom customer segmentation is easy-peasy! Next time that marketer asks for new segmentation rules, we just go back to our XML file and then take a nice a KitKat break.

screen-capture-6.png

 

Obviously, there's plenty of room for improvement here. Ideally, we make the tags and UI select membership and schools from pre-defined lists. Also, I'm not too fond of having to edit XML files and redeploying the application to handle changes, so we could move the profile attribute configuration into Elastic Path's Settings Framework so that they can be modified at runtime without having to restart. But this is a good starting point, and hopefully that gives you an idea of how easy it is to extend the Tagging Framework to meet some common pricing and personalization scenarios.

 

Segment away!

0 Comments Permalink

This post walks through adding filtering to the System Configuration page in the CM Client helping you track down a setting as quickly as possible.

 

We added the Settings Framework in Elastic Path Commerce 6.1 and it's proved incredibly useful in centralizing configuration and easing customization versus the previous XML based approach.  The number of settings out-of-the-box continues to grow plus any additional ones that customers are free to add.  So this tip will show you how to reign in that growing number of settings.

 

Here's a screenshot of what we'll be producing, if you've spent any time in the System Configuration (like our trusty QA guys) this will be a real time saver.

 

settings-filter.png

 

Changing the layout

I had to refresh my SWT widget layout knowledge a little and found this great page: http://www.eclipse.org/articles/article.php?file=Article-Understanding-Layouts/index.html.  With that excellent refresher I decided I would need a three column GridLayout: a column each for the Edit button, filter label and the filter text widget.  The table would then span all three columns.

 

settings-filter-layout.png

First, simply change the number of columns on the SettingDefinitionComposite:

 

private void setupLayout() {
     final int columns = 3;
     this.setLayout(new GridLayout(columns, false));
}

 

Then add in a horizontal span to the table's LayoutData - this makes it span the three columns we just created.

final int horizontalSpan = 3;
table.setLayoutData(new GridData(GridData.FILL, GridData.FILL, true, true, horizontalSpan, 1));

Note: the final variables are to keep Checkstyle quiet about magic numbers.

 

Adding the new widgets

The code below adds the label and the filter Text widget.  Take a close look at the style we are using to create the Text widget with: SWT.SEARCH | SWT.CANCEL.  The SWT.SEARCH style gives us the rounded edges (at least on my Mac) which makes it look like a regular search/filter widget.  The SWT.CANCEL style adds the small cross to the widget.  When clicked it removes the currently entered text.

Label label = formToolkit.createLabel(this, AdminConfigurationMessages.filterLabel + ":"); //$NON-NLS-1$
GridData gridData = new GridData(SWT.END, SWT.CENTER, false, false);
label.setLayoutData(gridData);
Text settingNameFilter = formToolkit.createText(this, "", SWT.SEARCH | SWT.CANCEL); //$NON-NLS-1$
gridData = new GridData(SWT.FILL, SWT.CENTER, false, false);
final int verticalIndent = 10;
gridData.verticalIndent = verticalIndent;
settingNameFilter.setLayoutData(gridData);

 

We want this new addition to be localizable so we need to add AdminConfigurationMessages.filterLabel

public static String filterLabel;

 

And then we provide the English version of that in AdminConfigurationPluginResources.properties

 

filterLabel=Filter

 

Filtering

Now we'll take a look at how we will actually filter down the table's contents.  The code's pretty simple:

/**
 * Filter setting definitions against a specified string.
 */
private class SettingPathFilter extends ViewerFilter {
     private final String filterText;
     public SettingPathFilter(final String filterText) {
          this.filterText = filterText;
     }
     @Override
     public boolean select(final Viewer viewer, final Object parent, final Object element) {
          SettingDefinition definition = (SettingDefinition) element;
          return StringUtils.containsIgnoreCase(definition.getPath(), filterText);
     }
}

We've simply extended the JFace ViewerFilter class and implemented the select method with a case-insensitive check against the setting definition's path.  In the next step we'll create an instance of this class and pass it to the TableViewer that holds the setting definitions.

 

The following page helped me a bit with the ViewerFilter: http://www.java2s.com/Code/Java/SWT-JFace-Eclipse/DemonstratesListViewer.htm

 

Hooking it together

The bits are all in place, let's tie them together and get the filtering going when a user types in the text box.  We simply add a ModifyListener to the filter Text widget and when we receive a ModifyEvent we grab the user-entered string, create a new SettingPathFilter and set that on the tableViewer.  That triggers the tableViewer to filter its contents.  Note: I chose to use setFilters, rather than addFilter/removeFilter, to keep the code simple: I don't have to keep track of the filter to remove it afterwards I simply call setFilters again.

 

settingNameFilter.addModifyListener(new ModifyListener() {
     public void modifyText(final ModifyEvent event) {
          final Text source = (Text) event.getSource();
          String filterText = source.getText();
          if (StringUtils.isBlank(filterText)) {
               tableViewer.setFilters(new ViewerFilter [0]);
          } else {
               tableViewer.setFilters(new ViewerFilter [] {new SettingPathFilter(filterText)});
               }
     }
});

 

 

Conclusion

So there you have it, all done, with a few small changes we can find settings without having to visually scan the table.  Where else might this filtering function be useful?  Anyone done any similar customizations they would like to share?

 

The code

The code changes and the attached patch are against the upcoming 6.2 release, but I'm pretty sure they will apply to any 6.1+ version.  Leave a comment if you have any problems applying this and I'll do my best to help you out.

References

http://www.eclipse.org/swt/snippets/ - if you've not taken a look at the SWT snippets then you're missing out.  There's a ton of useful examples of using SWT.

http://wiki.eclipse.org/index.php/JFaceSnippets - just like the SWT snippets, this page contains great snippets about using the JFace ui toolkit.

2 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

Alternative title: How to Make Your Life Easier and Not Go Bald from Pulling Your Hair out Trying to Keep Current on a Constantly Evolving and Shifting Codebase

 

This is probably the biggest and most avoided question from all of our successfully launched Professional Services projects. How can we upgrade to the greatest and latest on our heavily customized EP project? Let's be clear that performing a point release upgrade, or even consuming the latest sprint of code from our extremely productive Product Development group isn't trivial. A full version upgrade is even more daunting and usually heavily balanced against the cost of re-implementing from scratch.

 

First off, hopefully you are following our advice to try to avoid customizing EP code as far as possible, as noted in our documentation and tutorials. Following this methodology will make your life a lot easier, as upgrading core EP classes that have only minor changes will be a lot easier.

 

With that in mind, the best method through tried and tested experience, from brute force patching and merging, to re-implementing large customizations, is to take advantage of the merge tools from your chosen source control software (you are using source control I hope! If not, this book will be your best friend). Why do all the manual merge work when X does it so well already (X being subversion, perforce, cvs, vss, git, the next big flavor of the month). The key to leveraging your source control software to facilitate an upgrade is the concept of a vendor branch.

 

Working with a vendor branch is straight forward. This is applicable to any 3rd-party library you are using and modifying, not just EP. Take a quick gander at these various interweb sites for a better explanation than i can reasonably provide. For our purposes, a vendor branch for EP is an untainted, pristine copy of EP source. Don't just take our beloved zip of source code and stash it in your network drive! Check it in and show it off! You can branch your customized codeline off the vendor branch and start working away with the enthusiasm of a frenzied squirrel in the spring. The next time a new version of EP comes along, simply drop it directly on top of the current vendor branch of EP in your repository. With this new EP revision, you can perform your normal merge routine of just the diff between these EP revisions and merge the delta of changes into your customized eCommerce solution. And, if you have been following the procedures to avoid modifying EP source, conflicts should be kept to a minimum, files will just resolve themselves, and you can go out for a quick pint. Sure sounds easy huh? But how about in real life?

 

Well, let's see how we've taken this and made our lives easier at a client site in Perforce. I'm not the biggest Perforce fan, but I sure appreciate it's advanced branching and visual merging capabilities. We've set this up as follows:

 

  • EP_IMPORT, our EP vendor branch containing 100% pure EP 6.1 eCommerce goodness
  • Stable - our main development branch, full of customizations of the greatest and latest variety

 

Now, presume we're taking in change after massive change from our Product Development group. They've given us EP 6.1.1! Hooray! But how do we get this into our source control? Looks easy enough. Drop it on top of EP_IMPORT and check it in? Whoa, hold on there, code cowboy. This baby probably won't compile just with that.

 

The main things to remember is to collect three things with each new drop of code:

 

  1. All files that have been updated in 6.1.1
  2. All files that have been deleted in 6.1.1
  3. All files that have been added in 6.1.1

 

Once you have all three, you can mark the relevant files for delete, add and update. I've taken a short cut to calculate the files that fall into the above three buckets in this case. I have access to our internal subversion repository at EP, so I'm just going to take a log of files taken in by 'svn update' when updating from the 6.1 tag and the new 6.1.1 tag. You can essentially calculate the same bucket of files using the diff command. But in my case I'm just taking a shortcut and doing:

 

svn update > merge.txt

 

This logs out for us a lot of lines of this variety:

U    com.elasticpath.core/WEB-INF/src/main/java/com/elasticpath/service/rules/PromotionRuleDelegate.java
D    com.elasticpath.core/WEB-INF/src/main/java/com/elasticpath/service/misc/impl/WorkAroundOpenJPAFetchPlanHelperImpl.java
A    com.elasticpath.core/WEB-INF/src/main/java/com/elasticpath/service/misc/impl/OpenJPAEventListeningFetchPlanHelperImpl.java

 

With some help from sed (for the non-Unixers, feel free to grab and install cygwin, or your favorite port of the beloved Unix command line tools), I'm going to parse the log and split it into three files:

cat merge.txt | sed \-e '/D /d' | sed \-e '/U /d' | sed \-e 's/A[ ]*//' > adds.txt
cat merge.txt | sed \-e '/A /d' | sed \-e '/U /d' | sed \-e 's/D[ ]*//' > deletes.txt
cat merge.txt | sed \-e '/A /d' | sed \-e '/D /d' | sed \-e 's/[ ]*[U]*[ ]*//' > updates.txt

 

Now, Perforce is a bit finicky in that I need to mark which files I'm going to edit to remove the read-only permissions of relevant files. Sure, I can mark every file for editing, but let's play it safe:

pwd
/perforce/depot/branches/EP_IMPORT/
cat updates.txt | xargs p4 edit -f

 

Next, time to mark the deleted files for removal:

cat deletes.txt | xargs p4 delete -f

 

Finally, it's time to unzip on top all the files from the new EP 6.1.1 source zip and mark our new files for add:

tar -xvzf EP6.1.1_Export.tgz .
cat adds.txt | xargs p4 add -f

 

That does it! Now check that all in! Now we have EP 6.1.1 in our EP_IMPORT branch. Time to use the impressive P4V tool from Perforce to integrate from the EP_IMPORT branch into the Stable branch. Since we've avoided modify EP source as much as possible, the majority of the changes should auto-resolve. Clean up some of the conflicts and tweak the customizations as necessary, run the normal routine of compile, checkstyle, pmd, unit tests, integration tests and your project should now be ready to go. Okay, that's a gross over-simplication of the conflict resolution and regression testing process that goes hand in hand with a version upgrade, but hopefully you see how a vendor branch does wonders in facilitating an easier upgrade process with custom code. No more awkward patch files and brute force.

 

Assuming you don't have access to our subversion to get a hold of the list of changes, you can use diff to log file differences and files missing from the 6.1 and 6.1.1 codebases.

 

diff -r /perforce/depot/branches/EP_IMPORT /tmp/EP611/ > diffs.txt

 

This should give you a nice log that is easily parsed into the same add/delete/update buckets. So now you're freshly armed to do the upgrade that has been haunting you late at night. Vendor branches and merge tools are your new best friends! Happy upgrading and merging.

 

Drew

0 Comments Permalink