Customizing Elastic Path is usually simple. Customizing Elastic Path the right way takes some more thought. One of the best ways to customize EP is by using the decorator design pattern. Decorators, aka. "wrappers", are a great way to add functionality (i.e. customizations) to an object without modifying the object itself.

 

First, why decorate a class? Isn't it easier to just modify the class itself?


There are a number of advantages to using decorators:

  • You may not have access to the source code; wrapping the class you wish to customize may be your only option.
  • In addition to the customized behavior of a class, you may wish to retain the original OOTB behavior.
  • The less EP code you modify, the smoother future EP upgrades will be!
  • Wrapping a class ensures that the unit/integration tests for that class will not break.
  • Decorators are easy to understand and are a great example of object-oriented programming.

 

 

I recently developed a customization for a Professional Services project involving the new Settings Framework (available in Elastic Path Commerce 6.1). You can read more about the Settings Framework here, but what you need to know for now is that settings are strings stored in the database and identified by a PATH. All settings have a default value. In addition to the default value, a setting can have multiple context values. For example, consider a setting defined by the path CONFIG/siteAddress that has the default value ep.com. This setting also has two contexts: one for SnapItUp where the value is snapitup.ep.com and one for SLRWorld where the value is slrworld.ep.com.

 

The two context-specific values as well as the default context-less value for the CONFIG/siteAddress setting is conveyed in the following table:

 

Context
Value
<no context>ep.com
SnapItUpsnapitup.ep.com
SLRWorldslrworld.ep.com

 

The SettingsService provides a means to retrieve settings from the database.


Based on our data set above, the following service calls return different values:

  • SettingsService.getSettingValue("CONFIG/siteAddress").getValue() returns "ep.com"
  • SettingsService.getSettingValue("CONFIG/siteAddress", "SnapItUp").getValue() returns "snapitup.ep.com"
  • SettingsService.getSettingValue("CONFIG/siteAddress", "SLRWorld").getValue() returns "slrworld.ep.com"


Very useful, but our PS project requires environment-specific settings. Not only do we have multiple stores (i.e. contexts), each store also has multiple environments. For instance, the Snap It Up and SLR World stores each have 2 environments: a test environment and a production environment; each of these environments have a different CONFIG/siteAddress setting value.

 

Here is our dataset:

 

Context
Test environment value
Production environment value
<no context>ep.comep.com
SnapItUpsnapitup.test.ep.comsnapitup.ep.com
SLRWorldslrworld.test.ep.comslrworld.ep.com

 

So, we actually want SettingsService.getSettingValue("CONFIG/siteAddress", "SnapItUp").getValue() to return different values depending on which environment it's being called in; it should return snapitup.test.ep.com when called from the test environment and snapitup.ep.com when called from the production environment. This feature is not supported OOTB.

 

You could implement this by modifying the SettingsService class. Here is the SettingsService.getSettingValue() method in pseudocode form:

String getSettingValue(String path, String context) {
    // Check the database for a setting value that has the given path and the given context. 
    String settingValue = database.getSettingValue(path, context);
 
 
    if (settingValue != null)
        return settingValue;
    // If a setting value with the given context does not exist, then return the default context-less value. 
    return database.getSettingValue(path);
}

 

Modifying this method and making it environment-specific is easy:

String getSettingValue(String path, String context) {
    // The environment itself is stored in a setting.
    String environment = this.getSettingValue("SYSTEM/ENVIRONMENT");
    String envSpecificContext = environment + "/" + context;   
 
 
    // Check the database for a setting value that has the given path and the given context. 
    String settingValue = database.getSettingValue(path, envSpecificContext);
 
 
    if (settingValue != null)
        return settingValue;
 
    // If a setting value with the given context does not exist, then return the default context-less value. 
    return this.getSettingValue(path);
}

 

What did we do? We first grabbed the environment value from another setting (SYSTEM/ENVIRONMENT). We then prepended the environment onto the given context. Next, we looked in the database for a setting value with a context in the form <environment>/<context>. This code will work as long as our database contains the following:

 

ContextValue
<no context>ep.com
test/SnapItUpsnapitup.test.ep.com
test/SLRWorldslrworld.test.ep.com
production/SnapItUpsnapitup.ep.com
production/SLRWorldslrworld.ep.com

 

But there are problems with modifying the SettingsService class like this:

  • There's no way to retrieve non-environment-specific settings now.
  • Any existing SettingsService test-cases are probably broken and will need to be fixed.
  • If the next version of EP has modifications to the SettingsService class, there will be merge conflicts when this code is upgraded.


Here's how we can implement this customization by decorating the SettingsService instead:

class SettingsServiceDecorator implements SettingsService {
 
 
    private SettingService settingsServiceComponent;
 
 
    // Spring injector method. 
    void setSettingsServiceComponent(SettingsService settingsService) {
        this.setttingsServiceComponent = settingsService;
    }
 
 
    // This method has the same behavior of the component class; thus, just delegate to the SettingsService.
    String getSettingValue(String path) {
        return this.settingsServiceComponent.getSettingValue(path);
    }
 
 
    ...
 
 
    String getSettingValue(String path, String context) {
        String environment = this.getSettingValue("SYSTEM/ENVIRONMENT");
        String envSpecificContext = environment + "/" + context;
 
 
        return this.settingsServiceComponent.getSettingValue(path, envSpecificContext);
    }
}

 

Here is a high-level diagram of what we've done:

decoratorDiagram.jpg

 

Things to note here:

  • The new SettingsServiceDecoratorImpl class implements the same SettingsService interface of the class that it's decorating, SettingsServiceImpl.
  • This class has a reference to the class it is decorating; it's "wrapping" the SettingsServiceImpl class. 
  • Methods like getSettingValue(String) that have the same behavior as the component class simply delegate to the component class's method.

 

We now have an environment-specific settings service in addition to the OOTB settings service. When an environment-specific setting needs to be retrieved, we'll use the decorated class and when a non-environment-specific setting needs to be retrieved, we'll just use the OOTB class.

// Retrieve an environment-specific setting value. 
 
SettingsService settingsServiceDecorator = new SettingsServiceDecoratorImpl();
String envSpecificSetting = settingsServiceDecorator.getSettingValue(path, context);
 
 
// Retrieve a normal non-environment-specific setting value. 
 
SettingsService settingsService = new SettingsServiceImpl();
String nonEnvSpecificSetting = settingsService.getSettingValue(path, context); 


Pretty nice. Let's go one step further. EP classes that need to retrieve settings have a SettingsService injected into them via Spring Dependency Injection. Remember, the new SettingsServiceDecoratorImpl class implements the same interface as the original SettingsServiceImpl class. So if the ProductService, for example, requires the new environment-specific settings service, we do not even have to modify the ProductService class. We need only modify Spring's ApplicationContext.xml file (or service.xml in Elastic Path's case).

 

Before:

<bean id="productService" parent="txProxyTemplate">
     <property name="target">
          <bean class="com.elasticpath.service.catalog.impl.ProductServiceImpl">
               ...
               <property name="settingsService">
                    <ref bean="settingsService"/>
               </property>
          </bean>
     </property>
</bean>

<bean id="settingsService" parent="txProxyTemplate">
     <property name="target">
          <bean class="com.elasticpath.service.settings.impl.SettingsServiceImpl">
            <property name="settingsDao" ref="settingsDao"/>
            <property name="settingValueFactory" ref="settingValueFactory"/>
       </bean>
     </property>
</bean>

 

After:

<bean id="productService" parent="txProxyTemplate">
    <property name="target">
          <bean class="com.elasticpath.service.catalog.impl.ProductServiceImpl">
              ...
              <property name="settingsService">
                    <ref bean="settingsServiceDecorator"/>
              </property>
          </bean>
    </property>
</bean>


<bean id="settingsService" parent="txProxyTemplate">
    <property name="target">
          <bean class="com.elasticpath.service.settings.impl.SettingsServiceImpl">
               <property name="settingsDao" ref="settingsDao"/>
            <property name="settingValueFactory" ref="settingValueFactory"/>
          </bean>
    </property>
</bean>

<bean id="settingsServiceDecorator" parent="txProxyTemplate">
     <property name="target">
          <bean class="com.elasticpath.service.settings.impl.SettingsServiceDecoratorImpl">
               <property name="settingsServiceComponent" ref="settingsService"/>
          </bean>
     </property>
</bean>

 

What did we do?

  • The ProductService is injected with a SettingsService in both cases. In the before, that SettingsService is an instance of SettingsServiceImpl. In the after, that SettingsService is an instance of SettingsServiceDecoratorImpl. The ProductService itself doesn't care as long as the instance implements the SettingsService interface!
  • The new SettingsServiceDecoratorImpl has an instance of SettingsServiceImpl injected into it; this is the class that it's wrapping.

 

As you can see, using the decorator design pattern is a great way to customize Elastic Path without breaking existing behavior. And by not directly modifying core application code, you're helping to ensure smooth upgrades in the future.

0 Comments Permalink

What if your online store could identify shoppers with mobile devices and deliver content based on the device they're using? With the arrival of Dynamic Content and the Tagging Framework in Elastic Path Commerce 6.1.1, you can do just that.

 

This article takes a practical approach to extending the Tagging Framework. We'll look at how a developer can add a tag to identify mobile device users and how to create a tagging event listener to capture the information we need to store in that tag. And it's really quite easy, I promise. (Before diving in, you'll need a basic understanding of the Tagging Framework and Dynamic Content, so you'll want to take a look at my earlier posts and check out the 6.1.1 documentation.)

 

Solution Overview

When a visitor views a page on your Elastic Path storefront, the HTTP request includes a User-Agent header. The User-Agent header contains information about the application that's requesting
the page. This could be an automated web crawler, such as Googlebot or, if there's a person using that application to view web pages, a web browser. In the case of requests coming from mobile devices, like the iPhone, the user agent header also includes the name of the device.


We need to store this information in a tag and attach it to the shopper so that it can be accessed later on when we need to determine what Dynamic Content to display. Elastic Path Commerce doesn't have a tag that contains this information by default, so we need to do three things:

  • define a USER_AGENT tag in the "who" tag dictionary
  • create a tagging event listener (a tagger) class to get the User-Agent header from the request and store it in a USER_AGENT tag in the shopper's tag set
  • add the listener to the collection of event listeners that are invoked each time an HTTP session is created in Elastic Path Commerce.

 

Once this is done, marketing users will be able to target Dynamic Content based on the type of mobile device the shopper is using.

Defining the USER_AGENT Tag

First, we need to define the USER_AGENT tag. To do this, we need to insert a row into the TTAGDEFINITION table of the Elastic Path Commerce database:

insert into ttagdefinition(uidpk, guid, name, description, data_type)
values(10, 'USER_AGENT', 'USER_AGENT', 'The User-Agent request header.', 'java.lang.String');

Next, we need to add the tag to one of the three tag libraries ("who", "when", and "where"). The user agent is a piece of information that helps us identify the shopper, so we need to add it to the "who"
library. To do this, we need to insert a row into the TTAGDICTIONARYTAGDEFINITION table:

insert into ttagdictionarytagdefinition(tagdictionary_guid, tagdefinition_guid)
values('WHO', 'USER_AGENT');

Now, marketing users can log in to the Commerce Manager client, create Dynamic Content, and configure the Dynamic Content Delivery to evaluate the USER_AGENT tag, but at this point that wouldn't be very useful, because we haven't yet set up the tagging event listener to get that information and store it in the shopper's tag set.

Creating the Tagger

There are two tagging event listener interfaces defined in the Elastic Path Commerce core library, under com.elasticpath.commons.listeners:

  • NewHttpSessionEventListener, which is fired when the visitor arrives at the storefront and an HTTP session is created.
  • CustomerLoginEventListener, which is fired when the visitor signs in to their store account.

 

Both interfaces expose an execute method that takes a CustomerSession and an HttpServletRequest as parameters.

 

In the storefront web application, the com.elasticpath.sfweb.listeners package contains several Tagger classes that implement one or both of the listener interfaces. These classes are responsible for populating the tags with the data they need. We can get the User-Agent header at the same time as the session is created, so we'll create a UserAgentTagger class that extends NewHttpSessionEventListener. The interface defines only one method: execute. We'll implement the execute method to capture the user agent header string and put it in a tag:

package com.elasticpath.sfweb.listeners; 
 
 
import org.apache.log4j.Logger;
import  javax.servlet.http.HttpServletRequest;
import com.elasticpath.commons.listeners.NewHttpSessionEventListener;
import com.elasticpath.domain.customer.CustomerSession;
import com.elasticpath.tags.Tag;
import com.elasticpath.tags.TagSet;
 
public class UserAgentTagger implements NewHttpSessionEventListener {
     private static final String USER_AGENT = "USER_AGENT";
     private static final Logger LOG = Logger.getLogger(UserAgentTagger.class);
 
     public void execute(CustomerSession session, HttpServletRequest request) {
 
          // get the user agent header string
          String userAgent = request.getHeader("User-Agent");
          
          if (LOG.isDebugEnabled()) {
               LOG.debug("User-Agent header contents: " + userAgent);
          }
 
          // get the shopper's tag set
          TagSet tagSet = session.getCustomerTagSet();
 
          // set the user agent in the shopper's tag set
 
          Tag userAgentTag = new Tag();
          userAgentTag.setValue(userAgent);
          tagSet.addTag(USER_AGENT, userAgentTag);
     }
}

Adding the Tagger to the Event Listeners Collection

When an HTTP session is created, the handleFilterRequest method of webCustomerSessionService calls each listener's execute method. So, we need to add the user agent tagger to the newHttpSessionEventListeners collection on the webCustomerSessionService bean. Open the storefront web app's serviceSF.xml file (located in WEB-INF/conf/spring/service) and add a bean definition for the user agent tagger:

 

<bean id="userAgentTagger" class="com.elasticpath.sfweb.listeners.UserAgentTagger"/>

 

Next, find the webCustomerSessionService bean definition. Inside bean definition, find the newHttpSessionEventListeners property and add a reference to the userAgentTagger bean:

 

 

We can now test this by enabling debug logging on the storefront and then accessing the storefront with various browsers. Each time a new customer session is created, a debug message is logged with the complete user agent header string. For example, when I use my iPod Touch to access my storefront, the following message is logged:

2009-04-22 17:48:26,246 [http-8080-Processor23] DEBUG com.elasticpath.sfweb.listeners.UserAgentTagger - User-Agent header contents: Mozilla/5.0 (iPod; U; CPU iPhone OS 2_2_1 like Mac OS X;
en-us) AppleWebKit/525.18.1 (KHTML, like Gecko) Version/3.1.1 Mobile/5H11 Safari/525.20

There's a lot more information in there than we really need, but we see that it contains the word iPhone. So, if we want to target content at shoppers who are using an iPhone or an iPod, we can now do that.

Now, when we're creating the Dynamic Content Delivery, we can select the USER_AGENT tag and use its value as one of the criteria for displaying content. For example, if you're configuring Dynamic Content Delivery to show a different banner to iPhone users, add the USER_AGENT tag with the operator set to includes and the value set to iPhone.

dcd-user-agent.png

This is just one way you can extend the Tagging Framework. You could also create a tag to get information from a cookie or a specific request parameter. And you're not limited to information in the HTTP request; for example, you could tag customers with products and categories they've viewed. Going forward, we'll be building more tags into the framework, so if you've got more ideas, we'd love to hear about them.

 

I know I've probably glossed over a few details, so if you have any questions or comments, please don't hesitate to post them here.

0 Comments Permalink

Tagging Framework 101

Posted by Paul Monk May 11, 2009

The Tagging Framework is a new feature in 6.1.1 that allows you to track information about objects in Elastic Path Commerce. Objects can have tag sets, which can contain tags. Each tag contains a piece of information about the object. The values in the tags can be evaluated against a set of rules (called conditional expressions). The outcome of the evaluation can then be used to make decisions about application behavior.

 

In 6.1.1, the Tagging Framework is used by Dynamic Content Delivery conditions to determine which Dynamic Content is displayed to the shopper, but there are many other potential applications for it. This article looks at some of the main parts of the Tagging Framework. Aside from the javadoc (and this article), the inner workings of the Tagging Framework are currently undocumented, but hopefully, this article will give you a bit of a start.

Main Classes

The classes and interfaces of the Tagging Framework are located in the core library (com.elasticpath.core) under the com.elasticpath.core.tags package.

  • Tag: a piece of information about an object

  • TagSet: a set of tags associated with an object

  • TagDefinition: defines a piece of information that can be stored and tracked by the application

  • TagDictionary: a set of tag definitions

  • Condition: a rule that can be used to evaluate tag values

  • ConditionalExpression: a group of Conditions, joined by a logical operator (AND, OR, or NOT) and evaluated as a single unit

  • ConditionDSLBuilder: a service used to convert conditional expressions to strings and vice versa. The strings contain the expression in Elastic Path's domain specific language (DSL) for tag evaluation.

  • ConditionEvaluatorService: a service that evaluates a set of tags against a conditional expression and returns true or false.

The service beans are defined in WEB-INF/conf/spring/service/service.xml.

Database Tables

The following database tables are used by the Tagging Framework:

  • TTAGALLOWEDVALUE: can be used to restrict the values allowed for a specific setting

    • TTAGCONDITION: contains conditional expressions

  • TTAGDEFINITION: contains the definitions of the tags supported by the application

    • TTAGDICTIONARY: contains the available tag dictionaries ("who", "when", and "where")

  • TTAGDICTIONARYTAGDEFINITION: associates tags with tag dictionaries.

In addition, the TSELLINGCONTEXTCONDITION table is used to associate conditional expressions stored in TTAGCONDITION with Dynamic Content Delivery.

Defining a Tag

Before you can use a tag, you must define it and add it to a tag dictionary. To define a tag, insert a row into the TTAGDEFINITION table of the Elastic Path Commerce database:

insert into ttagdefinition(uidpk, guid, name, description, data_type)

values(10, 'USER_AGENT', 'USER_AGENT', 'The User-Agent request header.',

'java.lang.String');

The data_type field must contain a valid, fully-qualified Java class name (java.lang.String, java.lang.Integer, etc.).

Adding a Tag to a Tag Dictionary

After a tag is defined, you need to add it to at least one of the three tag dictionaries ("who", "when", and "where"). The "who"  tag dictionary defines tags that describe a shopper. The "where" tag dictionary defines tags that identify the selling channel (which stores). The "when" tag dictionary defines tags that indicate the time. To add a tag to the "who" tag dictionary, insert a row into the TTAGDICTIONARYDEFINITION table:

insert into
ttagdictionarytagdefinition(tagdictionary_guid, tagdefinition_guid)
values('WHO', 'USER_AGENT');

 

Adding a Tag to a Tag Set

The tag set associated with a shopper can be retrieved from the customer session object. The following code retrieves the tag set, creates a tag, and adds the new tag to the tag set.

import com.elasticpath.tags.TagSet;

import com.elasticpath.tags.Tag;

...

CustomerSession session = shoppingCart.getCustomerSession();

TagSet tagSet = session.getCustomerTagSet();

Tag newTag = new Tag();

newTag.setValue("A value");

tagSet.addTag("NEW_TAG", newTag);

 

Usually, similar code would be executed inside a tagging event listener.

Implementing a Tagging Event Listener

The Tagging Framework uses an event listener model to populate tag values. Listeners are invoked by the application when specific events occur, such as when the shopper logs on to their store account. To populate a tag, you create a listener and add it to the appropriate collection.

Two types of listeners are supported by Elastic Path Commerce out of the box:

  • NewHttpSessionEventListener: these listeners are called when the visitor arrives at the storefront and an HTTP session is created.

  • CustomerLoginEventListener: these listeners are called when the visitor signs in to their store account.

Both listener interfaces expose an execute method that takes a CustomerSession and an HttpServletRequest as parameters.

To add a new tag, you first need to determine how the value of the tag will be set. If the value of the tag can be retrieved from the HTTP request, your listener class should implement NewHttpSessionEventListener. If the value of the tag is only available after the customer has logged on to their Elastic Path account, then create a listener class that implements CustomerLoginEventListener. The class must implement the execute method to set the tag value and add it to the tag set.

package com.elasticpath.sfweb.listeners;
 
import javax.servlet.http.HttpServletRequest;
import com.elasticpath.commons.listeners.NewHttpSessionEventListener;
import com.elasticpath.domain.customer.CustomerSession;
import com.elasticpath.tags.Tag;
import com.elasticpath.tags.TagSet;
 
public class UserAgentTagger implements NewHttpSessionEventListener {
 
     private static final String USER_AGENT = "USER_AGENT";
     
     public void execute(CustomerSession session, HttpServletRequest request) {
          // get the user agent header string
          String userAgent = request.getHeader("User-Agent");
          
          // get the shopper's tag set
          TagSet tagSet = session.getCustomerTagSet();
          
          // set the user agent in the shopper's tag set
          Tag userAgentTag = new Tag();
          userAgentTag.setValue(userAgent);
          tagSet.addTag(USER_AGENT, userAgentTag);
     }
}

I'll be following up a little later this week with an example of a potential real-world use of the Tagging Framework and Dynamic Content.

1 Comments Permalink

We had a client who needed to display entire product categories (several hundred products in all) in a set of drop down list boxes on their Elastic Path Commerce storefront homepage. The data that had to be loaded was quite minimal, just the product codes, display names and a few localized attributes.

 

In this type of situation, using the default product loader to retrieve this information would not be the best approach. This is due to the fact that each Product domain object contains a large amount of data such as prices, skus, attributes, inventory and recommendations etc.  Loading all these details requires a significant number of queries to be run on the database so applying this approach to a homepage with hundreds of products would result in extremely poor performance.

 

For this customer, a better solution was to create a set of lightweight product display classes, and have these mapped directly to some custom JPA native queries. These queries were tailored to return only the specific product details needed and therefore avoid loading any unnecessary information.  To further reduce the number of database trips, we also introduced a timed cache in the storefront controllers which would store frequently accessed catalog items for a set period of time before being refreshed. 

 

The combination of these two techniques reduced their page response times from tens of seconds (using the default product loader) to under a few seconds even in the worst case scenario where a stale cache had to be refreshed.   Since most of the time the information would be available in the cache, the amount of database overhead was kept to a minimum.

 

If your storefront scenario has similar requirements, then the following code examples may also be useful for your project.

 

Using Native SQL Queries with JPA

To get all product codes and product names in a particular category, you can use the EntityManager's createNativeQuery method to create a native query with a WHERE clause that passes in the language string and a specific category UID.

 

final String productNamesByCatSql =

"SELECT tp.code, tpldf.display_name AS displayName"

+ " FROM tproduct AS tp"

+ " INNER JOIN tproductldf AS tpldf"

+ " ON tpldf.product_uid = tp.uidpk"

+ " INNER JOIN tproductcategory AS tpc"

+ " ON tp.uidpk = tpc.product_uid"

+ " WHERE tpldf.LOCALE = ?1"

+ " AND tpc.category_uid = ?2";

 

long categoryUid = getCategoryUid(request);

long start = System.currentTimeMillis();

 

// create a native SQL query

final Query query = entityManager.createNativeQuery(productNamesByCatSql, ProductDisplayBeanImpl.class);

 

// retrieve a simplified product list for a given category

final List<ProductDisplayBean> custProducts = (List<ProductDisplayBean>)

     query.setParameter(1, shoppingCart.getLocale().getLanguage())

     .setParameter(2, new Long(categoryUid))

     .getResultList();

 

if (LOG.isDebugEnabled()) {

     long elapsedTimeMillis = System.currentTimeMillis() - start;

     LOG.debug("No. of products returned = " + custProducts.size() + ", elapsed time (ms) = " + elapsedTimeMillis);

     for (ProductDisplayBean productBean : custProducts) {

          LOG.debug("[" + productBean.getCode() + ":" + productBean.getDisplayName() + "]");

     }

}

 

Like JPQL, native queries can be named for easy reuse using the @NamedNativeQuery annotation, however it's probably a better idea to externalize all the native query strings into an *orm.xml file so that the SQL can be updated without having to change the class files.  If you do that, you'll have to change all the createNativeQuery()calls to createNamedQuery().

 

A very simple display bean containing only the product name and code is needed just to pass the information to the storefront.

 

 

 

public interface ProductDisplayBean {

 

     public String getCode();

     public void setCode(String code);

     public String getDisplayName();

     public void setDisplayName(String displayName);

}

 

public class ProductDisplayBeanImpl implements ProductDisplayBean {

 

     public static final long serialVersionUID = 5000000001L;

     private String code;

     private String displayName;

 

     public String getCode() {

          return code;

     }

 

     public void setCode(String code) {

          this.code = code;

     }

     public String getDisplayName() {

          return displayName;

     }

     public void setDisplayName(String displayName) {

          this.displayName = displayName;

     }

}

Caching Frequently Accessed Items

 

Once you've retrieved a set of items from the catalog, you can easily cache these results at the application layer using either a third party solution, such as Ehcache (http://ehcache.sourceforge.net), or your own caching mechanism.  Elastic Path 6.1 already provides some out-of-the-box caching classes that you can use in your own code.  We normally use our SimpleTimeoutCache class to hold a list of frequently retrieved products that don't need to be updated very often. The following code sample shows how you could integrate SimpleTimeoutCache with your product retrieval service class.

 

private static final long CACHE_TIMEOUT = 30000;  // cache expires after 30 seconds

private final SimpleTimeoutCache<String, List<Product>> productsCache = new SimpleTimeoutCache<String, List<Product>>(CACHE_TIMEOUT);

...

String storeCode = shoppingCart.getStore().getCode();

List<StoreProduct> products = productsCache.get(storeCode);

 

// if the store code does not exist in the cache, then get it from the database

// and update the cache

if (products == null) {

     final IndexSearchResult productResults = retrieveProducts(browsingRequest, category);

     List<Long> productUids = getProductsUsingPageNumber(pageNumber, storeCode, productResults);

     products = getProductRetrieveStrategy().retrieveProducts(productUids,

     shoppingCart, productLoadTuner);

     productsCache.put(storeCode, products);

}

 

The timeout value can be adjusted for your requirements. A larger value will reduce the number of trips to the database but the trade-off is a longer wait time for catalog product changes to appear.

4 Comments Permalink

Users of the commerce manager client are created and maintained locally on the commerce manager server.  As shown below, the Commerce Manager client provides an interface to add, update and remove users:

ldap.jpg

 

For some organizations, this is sufficient for managing their user pool. Even when there are several users, a bulk import can suffice. However, this becomes burdensome when organizations have dozens or hundreds of users needing access to the Commerce Manager client. At this scale, an LDAP server is commonly used to manage an organization's user directory.


To integrate an LDAP server into the Commerce Manager server, there are some minor changes required to the login process and management functions that audit events. These changes achieve the following goals:

 

  1. Authentication process to connect to LDAP server.

  2. Audit trails to use LDAP users.

 

Elastic Path's Commerce Manager Server's security is based upon ACEGI, now known as Spring Security.  The following code snippets are updates to the acegi.xml file. They allow the Commerce Manager server to authenticate against a LDAP server:

 

   <bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">

        <property name="providers">

    <list>

         <ref local="ldapAuthenticationProvider" />

    </list>

</property>

    </bean>

<bean id="cmUserAuthenticationDao" class="com.elasticpath.persistence.impl.CmUserAuthenticationDaoImpl">

        <property name="persistenceEngine"><ref bean="persistenceEngine"/></property>

    </bean>

<bean id="ldapAuthenticationProvider" class="com.elasticpath.cmweb.security.impl.CmLdapAuthenticationProvider">

<constructor-arg><ref local="authenticator"/></constructor-arg>

<constructor-arg><ref local="populator"/></constructor-arg>

        <property name="userRoleService"><ref bean="userRoleService"/></property>

</bean>

<bean id="authenticator" class="org.acegisecurity.providers.ldap.authenticator.BindAuthenticator">

<constructor-arg><ref local="initialDirContextFactory"/></constructor-arg>

<property name="userDnPatterns">

<list>

<value>cn={0},ou=People</value>

</list>

</property>

<property name="userSearch"><ref local="userSearch"/></property>

</bean>

<bean id="initialDirContextFactory" class="org.acegisecurity.ldap.DefaultInitialDirContextFactory">

<constructor-arg value="ldap://10.10.1.1:389/dc=elasticpath,dc=com"/>

<property name="managerDn">

<value>cn=Manager,dc=elasticpath,dc=com</value>

</property>

<property name="managerPassword">

<value>password</value>

</property>

</bean>

 

<bean id="populator" class="org.acegisecurity.providers.ldap.populator.DefaultLdapAuthoritiesPopulator">

        <constructor-arg>

<ref local="initialDirContextFactory"/>

        </constructor-arg>

        <constructor-arg>

<value>ou=Groups</value>

        </constructor-arg>

<property name="groupRoleAttribute">

<value>ou</value>

</property>

        <property name="searchSubtree">

<value>true</value>

        </property>

        <property name="rolePrefix">

<value>ROLE_</value>

        </property>

        <property name="convertToUpperCase">

<value>true</value>

        </property>

    </bean>

   

<bean id="userSearch" class="org.acegisecurity.ldap.search.FilterBasedLdapUserSearch">

<constructor-arg>

<value>ou=People</value>

</constructor-arg>

<constructor-arg>

<value>(cn={0})</value>

</constructor-arg>

<constructor-arg>

<ref local="initialDirContextFactory" />

</constructor-arg>

<property name="searchSubtree">

<value>true</value>

</property>

</bean>

 

Upon authentication, an authentication token is returned to the Commerce Manager server from the LDAP server.  This token contains the user name, user password and list of user roles.  These values are used to create the Commerce Manager user in a method added to the CmUserServiceImpl class:

 

/**

* Creates a CmUser with the LDAP authentication token for use on the cm client.

*

* @param authToken the Authentication token created from the LDAP login information

* @return cmUser created from the Authentication token containing username, password and authorities

*/

public CmUser createLdapCmUser(final Authentication authToken) {

CmUser cmUser = getBean(ContextIdNames.CMUSER);

 

cmUser = initializeLdapCmUser(cmUser, authToken);

cmUser = saveOrUpdateLdapCmUser(cmUser, authToken);

return cmUser;

}

 

This public method calls two other methods which I named  initalizeLdapCmUser() and saveOrUpdateLdapCmUser().

 

initalizeLdapCmUser() instantiates a new user with the data returned in the authentication token.  The next method, saveOfUpdateLdapCmUser(), saves the new CmUser into the database.  This step allows the current implementation of auditing to associate a CmUser to a particular event.  Some refactoring is necessary to decouple the two and clean up the overall process, but that is for another day.


Configuring the users for roles is done by simply matching the role values created in the LDAP server to those created in the Commerce Manager.  For example, the following is an example of my LDIF file:

 

dn: dc=elasticpath,dc=com

objectClass: organization

objectClass: top

objectClass: dcObject

dc: elasticpath

o: elasticpath

 

dn: cn=Manager,dc=elasticpath,dc=com

objectClass: organizationalPerson

objectClass: person

objectClass: top

cn: Manager

sn: Manager

userPassword:: password

 

dn: ou=Groups,dc=elasticpath,dc=com

objectClass: organizationalUnit

objectClass: top

ou: Groups

 

dn: ou=SUPERUSER,ou=Groups,dc=elasticpath,dc=com

objectClass: groupOfNames

objectClass: top

cn: superuser

member: cn=user1,ou=People,dc=elasticpath,dc=com

ou: SUPERUSER

 

dn: ou=CMUSER,ou=Groups,dc=elasticpath,dc=com

objectClass: groupOfNames

objectClass: top

cn: cmuser

member: cn=user2,ou=People,dc=elasticpath,dc=com

ou: CMUSER

 

dn: ou=People,dc=elasticpath,dc=com

objectClass: organizationalUnit

objectClass: top

ou: People

 

dn: cn=ksuen,ou=People,dc=elasticpath,dc=com

objectClass: person

objectClass: top

cn: user1

sn: Kevin

userPassword:: password

 

dn: cn=kcleatharo,ou=People,dc=elasticpath,dc=com

objectClass: person

objectClass: top

cn: user2

sn: Maryam

userPassword:: password

 

I have defined a superuser and a cmuser group on the LDAP server.  I have also defined 'user1' and 'user2' belonging to superuser and cmuser respectively.  The value 'SUPERUSER' is matched and given the privilege of the corresponding role in the Commerce Manager.


If you have any questions, feel free to post a comment.

2 Comments Permalink

Dynamic Content 101

Posted by Paul Monk May 6, 2009

Dynamic Content is an exciting new feature in Elastic Path 6.1.1. The idea behind it is to give the Marketing team more control over site content without having to make changes to code (Java or HTML). There is still some initial work for the web developer to enable Dynamic Content in the storefront, and if your organization has special requirements, it's definitely possible for developers to extend the Dynamic Content framework (more on this in a future post), but control over Dynamic Content is mainly in the hands of Marketing. In a nutshell, here's how it works:

 

  • The web developer sets up some locations (called Content Spaces) within the web page for displaying Dynamic Content.
  • The EP administrator registers the Content Space in the Commerce Manager.
  • The Marketing person selects some content (a banner image, a Flash object) to display in the storefront.
  • The Marketing person chooses the Content Space where the content will appear and sets the conditions for displaying it (the who, when, and where).

 

And Elastic Path Commerce takes care of the rest. Let's take a closer look at how this works in practice.

 

Setting up a Content Space

The web developer creates the HTML page layout of the storefront in the Velocity templates. Within the HTML, the web developer can include the contentspace macro. This looks something like the following:

#contentspace('contentSpaceID', '<span>Alternative Content</span>')

This defines a Content Space, which can be used to display Dynamic Content. The first parameter is the Content Space ID, which will need to be registered before it can be used. The second parameter is the static content to display if there's no Dynamic Content mapped to the space.

 

If you have 6.1.1 installed and you've set up the demo store data, you can see some examples of Content Spaces in the storefront home page template (themes\electronics\default\templates\velocity\index.vm under your assets directory). For more information on setting up Content Spaces, see the Developer Guide.

 

Registering a Content Space

The EP administrator registers the Content Space in the Commerce Manager so that it can be used as a target for Dynamic Content. To do this, log in to Commerce Manager and choose Activity->Configuration. Then click Content Spaces under Targeted Selling. Click the Create Content Space button. Set the name to the Content Space ID used in the Velocity template. You should also enter a meaningful description, something that will help users determine where the Content Space is, its preferred dimensions, etc. For more details, see the Developer Guide or the Commerce Manager User Guide.

 

Creating Dynamic Content

Next, Marketing selects the type of content they want to display in the storefront. Out of the box, Elastic Path Commerce supports several types. For example, there are types for displaying images and Flash objects. There's also a type for displaying HTML. And developers can easily create more types. These types, also called Content Wrappers, aren't simply static content. They can include Groovy code to do just about anything you can imagine. And you don't need to restart the storefront to make changes to them. (You can find out more about custom Content Wrappers in the Content Wrappers section in the Developer Guide.)

dc_adding_dc4.png

After selecting a type, there's usually some configuration required. For example, for a clickable image type, you'd need to select an image file and set the URL where shoppers will be redirected when they click the image.

 

There's a tutorial that will walk you through the process of creating Dynamic Content. (For the tutorial, you'll need to either install the Trial Installer or to set up the demo store data in your dev environment.)

 

Setting up the Dynamic Content Delivery

The last step is to set up the conditions for displaying the content. We want to make sure that the content will be displayed in the right place (which Content Space), to the right shoppers, at the right time, and in the right store (for example, display this content to shoppers who followed a link from Google after searching for the keyword "clogs").

dc_adding_dcd6.png

The details of configuring Dynamic Content Delivery are covered in the tutorial on creating Dynamic Content and in the Commerce Manager User Guide.

 

Behind the scenes, Dynamic Content Delivery relies heavily on the Tagging Framework to get information about the shopper and decide which pieces of content to display. In future posts, we'll dig a bit deeper into the customizable aspects of both Dynamic Content and the Tagging Framework. Until then, be sure to check out the 6.1.1 documentation on the Elastic Path doc site for more in-depth coverage of Dynamic Content.

0 Comments Permalink

Elastic Path Commerce 6.1.1 is now available for download. This release includes bug fixes, performance enhancements, and some new features. Here's what's new and noteworthy:

  • Import/Export tool improvements (performance improvements, support for import/export of promotions and configuration settings)
  • Support for load balancing of search server requests
  • Dynamic Content
  • Tagging and tag evaluation (a.k.a., the Tagging Framework).

You can find out more by checking out the 6.1.1 Relase Notes and the 6.1.1 documentation on the Elastic Path documentation site.

 

Over the next few days, we'll be posting articles on the Grep technical blog to highlight some of the cool things you can do, particularly with Dynamic Content and the Tagging Framework. Dynamic Content is about personalizing the shopping experience; Marketers can configure what content to display to which shoppers, where it will appear in the storefront, and the time period during which it will be shown. The Tagging Framework is what enables Dynamic Content to be targeted at specific shoppers. When a shopper first arrives at the storefront, Elastic Path Commerce starts collecting information and storing that information in tags, including:

  • the URL of the site where the shopper came from
  • the storefront URL where the shopper arrived
  • the search terms that were entered in the search box (if the shopper came from a search engine page like Google or Yahoo)

And when the customer logs in, the Tagging Framework can pull in other information from the customer's account, such as gender and age. Marketers can create rules in the Commerce Manager to target specific Dynamic Content at specific shoppers based on the information in each shopper's tag set.

 

This is pretty exciting stuff, so be sure to stay tuned for blog posts!

0 Comments Permalink