Technical Blog

4 Posts tagged with the custom tag

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
Background

 

Our client's e-commerce store has been powered by Elastic Path for well over a year now. Our team started development of the store on a beta version of Elastic Path 6.0 in January 2008. After 3 months of development, including a merge with the public release of Elastic Path 6.0, we launched the store in March 2008. We continued to add new features and bug fixes. We even took advantage of EP 6.0's multi-store support to add a few more stores in August 2008. At the beginning of 2009, after a year's worth of customizations atop the client's EP 6.0 deployment, we decided that it was time to upgrade to EP 6.1.
timeline.jpg



Why Upgrade?

 

Upgrading a customized version of Elastic Path Commerce is not a trivial process. So you need to ask yourself (and the client) if an upgrade is even necessary. The first step of upgrading Elastic Path is determining whether the benefits of upgrading exceed the costs of upgrading. The cost is time and money.

 

What are the benefits? Here are some of ours:
  • EP 6.1 provides full multistore. In EP 6.0, multiple stores required multiple storefront WAR files: one for each store. EP 6.1 runs multiple stores from a single WAR file. Less WAR files translates to quicker deployments and a smaller resource footprint.  This is especially relevant to us since our client is running six stores.
  • EP 6.1 comes with 300+ bug fixes.
  • Upgrading to EP 6.1 paves the way for upgrades to future versions of Elastic Path. This is crucial, because our client is anticipating features that are planned in future versions of EP (e.g. import/export of promotion data, available in EP 6.1.1).
  • EP 6.1 comes with FIT integration tests.
  • EP 6.1 introduces the import/export utility, used to migrate catalog data between databases.
  • EP 6.1 introduces the settings framework, an improved way to manage configuration settings.
  • EP 6.1 allows search index rebuilding to be triggered from the CM Client.
  • The Store configuration UI has been overhauled in EP 6.1; the Store wizard has been replaced by a Store editor.
  • In EP 6.1, assets (e.g. velocity templates, images, etc.) are stored outside the web application, making the view layer much easier to modify.

 

We presented a list of EP 6.1 benefits to our client, provided an estimate on the amount of time it would take to complete the upgrade, and got the green light to go ahead with it. These were the benefits relevant to us, but EP 6.1 has many other improvements. For example, here are 7 Big Code Changes in EP 6.1.



The Upgrade

 

After we convinced ourselves and our client that upgrading to EP 6.1 was worthwhile, we moved on to the fun part: the upgrade itself. There are several steps that need to be completed during an upgrade and it's easy to become overwhelmed at first, so we broke the upgrade down into more manageable sub-tasks:
  1. Database updates
  2. Merging the code
  3. Moving customized named database queries
  4. Using the new settings framework
  5. Relocating the assets directory
  6. Setting up Maven
Let's cover each sub-task in more detail.

 

1. Database Updates
The database schema did not change significantly between EP 6.0 and EP 6.1. Elastic Path has a script that upgrades the database from EP 6.0.x to EP 6.0.3 and another that upgrades it from EP 6.0.3 to EP 6.1. We ran both of these scripts against our database with no problems. Most of the schema changes consists of new tables and new columns and there are few changes to existing columns. Click here for detailed information about the EP 6.1 database updates.

 

After upgrading the schema with the scripts, a few data updates needed to be made:
  • By default, full credit card numbers are stored in the database for each order; we turned this off (the ability to turn it off is a new EP 6.1 feature). This is accomplished by setting STORE_FULL_CREDIT_CARDS to 0 for each row in TSTORE.
  • Populate the new TSTORESUPPORTEDLOCALE table with store-locale mappings; these mappings mirrored the catalog-locale mappings already in TCATALOGSUPPORTEDLOCALE.
  • Populate the new TSTORESUPPORTEDCURRENCY table with the store-currency mappings; these mappings are similar to the catalog-currency mappings already in TCATALOGSUPPORTEDCURRENCY.

 

2. Merging the Code
The majority of the time spent on the upgrade was in this step: merging code. Generally, the more changes you make to a codebase, the harder it is to upgrade in the future. I say "generally" because there are ways to customize code that minimize future code merge conflicts. For example, this GREP technical blog post shows how to customize code with the decorator design pattern; a strategy that significantly reduces code conflicts.

 

The easiest way to merge large amounts of code is by using your version control software's merge tools combined with vendor branches. We have a great GREP article explaining what vendor branches are and how they can be used for merging code here. Since we use subversion for this particular project, we ended up using svn merge to merge the code.

 

This is the svn merge command we used:
svn merge https://svn.elasticpath.com/perfect_code/pd/ep5/tags/publicrelease6.0/ https://svn.elasticpath.com/perfect_code/pd/ep5/tags/publicrelease6.1/ . --accept postpone
This command takes the differences between OOTB EP 6.0 and OOTB EP 6.1 (whose vendor branches are specified in green and blue respectively), and applies those differences onto our customized codebase (specified in red, this command was run from our project's root level directory). The --accept postpone part tells the utility to postpone resolving any merge conflicts that occur.

 

The following diagram shows the branches referred to in the svn merge command and our desired "Customized EP 6.1" end state:
codemerge.jpg
When svn merge hits a conflicted file, it produces 3 output files. For example, consider a file called MyClass.java that is conflicted; svn merge will produce the files MyClass.java.left, MyClass.java.right, and MyClass.java.working. The highlighted colors correspond to the branches in the svn merge command above. The .left file is the EP6.0 version, the .right file is the EP 6.1 version, and the .working file is our customized codebase version.

 

The utility does a pretty good job at merging the source files. Some stats we gathered after running this command:
  • 3682 files were added
  • 118 files were deleted
  • 953 files were automatically merged successfully
  • 132 files were conflicted and not automatically merged
The high number of files added is largely attributed to the relocated assets directory and the new FIT tests. The 953 files merged successfully demonstrates the advantage of using a utility to automatically merge files. Unfortunately, not everything can be automatically merged; we had to manually merge the 132 conflicted files. These numbers will vary depending on the type and amount of customizations you've made on your codebase; our codebase has a lot of customizations.

 

There are many approaches to manually merging a conflicted file. The .left, .right, and .working files produced by svn merge, described above, are invaluable to this process.

 

This is my strategy:
  1. diff (i.e. compare) the .left file (EP 6.0 version) with the .right file (EP 6.1 version); this shows Elastic Path's changes to the file from version 6.0 to 6.1.
  2. diff the .left file (EP 6.0 version) with the .working file (your customized version); this shows your customizations to the file from 6.0 to its current state.
  3. Based on the comparisons above, who changed the file more? Elastic Path or you?
    • If Elastic Path made more changes to the file, start with the .right file and incorporate your changes (indicated by the diff in step 2) to the file.
    • If you made more changes to the file, start with the .working file and incorporate Elastic Path's changes (indicated by the diff in step 1) to the file.
    • If they have the same amount of changes, it doesn't really matter which version you pick as your starting point; the amount of work will roughly be the same. I lean towards picking the .right file (EP 6.1 version) because aligning your codebase to Elastic Path's will make future upgrades smoother.
  4. After merging the file in step 3, you are done. Move onto the next file!
More numbers. Of the 132 conflicted files:
  • 88 were relatively easy/trivial merges
  • 23 were moderately complicated merges
  • And 21 were very complicated merges
The very complicated merges were the files that were changed significantly by both us (i.e. our customizations) and by Elastic Path (during the development of EP 6.1). These files include the class files for ShoppingCart, OrderService, CheckoutService, and other areas of Elastic Path that are often modified.

 

All in all, the code merge is likely not as painful as most anticipate it to be. Even with 40+ files requiring non-trivial manual merges, our code merge went pretty smoothly.

 

A couple tips:
  • Take advantage of dry-run modes of your merging tool to preview how much work needs to be done without actually performing the merge. For example, adding the --dry-run option to our svn merge command enabled us to see how many merge conflicts we'd have to resolve in advance of the merge itself; this information is very useful during the estimation phase.
svn merge https://svn.example.com/project/tags/publicrelease6.0/ https://svn.example.com/project/tags/publicrelease6.1/ . --accept postpone --dry-run
  • As we manually merged the conflicted files, we took note of the merge details in a list. For each file that required non-trivial merging, we jotted down a couple sentences describing the merge (e.g. which methods were merged, etc.). Taking notes during the code merge requires a little overhead, but the list proved to be very useful when bugs caused by the merge were found later on.

 

3. Moving Customized Named Database Queries

 

In EP 6.0, named JPA database queries were embedded in the class files. For example, this bit of code is in CatalogImpl.java:
@NamedQueries({
    @NamedQuery(name = "CATALOG_IN_USE_BY_PRODUCT_TYPE",
            query = "SELECT pt.uidPk FROM ProductTypeImpl pt"
                    + " LEFT JOIN pt.catalog c WHERE c.uidPk = ?1")
    ...
})

In EP 6.1, the named JPA database queries have been moved from the class files to separate object-relational mapping XML files. The query above has been moved to the catalog-orm.xml file.
The code merge should take care of the OOTB (out-of-the-box) named queries but any customized name queries that we added had to be manually moved from the class file to the appropriate XML file. 


4. Using the New Settings Framework

 

In EP 6.0, configuration settings were stored in various XML files (e.g. commerce-config.xml, default.xml, etc.). EP 6.1 introduces a new settings framework that stores settings in the database and allows them to be managed from the CM Client; the setting values are retrieved wherever they are needed in the code via the SettingsService class. The new settings framework is a big improvement for managing configuration settings, but some work needs to be done to take advantage of it.

 

First, some of the OOTB setting values need to be overridden with your project's values. For instance, our client's stores use Elastic Path's one-page checkout. There is a boolean setting that turns this feature on. By default, this setting is off. In EP 6.0, we turned this setting on by modifying a line in commerce-config.xml: onepage.enable=true. In EP 6.1, this setting value is no longer retrieved from commerce-config.xml (although the value may still be there depending on how it was handled during the code merge), but it is not being used. The setting value is now retrieved from the database via the new settings framework. We located the setting (COMMERCE/STORE/enableOnePageCheckout) and changed its value from false to true with a database update. After we were done modifying the OOTB setting values, we removed all unused setting definitions from the XML files.

 

Second, we had to migrate all of the custom settings we created to the new settings framework. We have approximately 60 custom settings -- much more than typical projects. Migrating custom settings is not absolutely required since they are still parsed from XML files and retrievable using the ElasticPath class, but there are many advantages of using the settings framework.

To migrate a custom setting, you first need to create the setting definition with a database insert statement. For instance, the following insert statement creates our "tradeshow timeout" setting:
INSERT INTO TSETTINGDEFINITION(UIDPK, PATH, DEFAULT_VALUE, VALUE_TYPE, DESCRIPTION, MAX_OVERRIDE_VALUES)
 VALUES(81, "CUSTOM/tradeshowTimeout", "120000", "Integer", "Indicates the tradeshow timeout interval, in seconds.", -1);
Next, find all the places in the code that use this setting. Modify the code to use the SettingsService to retrieve the setting value from the database instead of from the XML files (via ElasticPath). Going back to the example above, we make the following javascript change:
timeoutId = window.setTimeout("resetTradeshowPage()", $ctxStoreConfig.getSetting("CUSTOM/STORE/tradeshowTimeout").getValue());
...and the following java code change:
final String settingValue = getSettingsService().getSettingValue("CUSTOM/STORE/tradeshowTimeout").getValue();
 
During our merge, we went one extra step and implemented environment-specific settings, a feature that is not available out of the box and that required some additional customization. You can read more about environment-specific settings in this technical blog post on GREP.

 

5. Relocating the Assets Directory

 

In EP 6.1, the assets directory was moved outside of the war file. For example, storefront templates were previously located in com.elasticpath.sf/WEB-INF/templates/; they are now located in assets/themes/storecode/default/templates. Note that the storefront templates were not the only ones that were relocated; CM templates, like those used for order confirmation emails, were also moved from the com.elasticpath.sf project to the new assets directory. The relocation decouples the view (i.e. templates, images, etc.) from the model and controller (the application). There are many advantages to this, including the ability to easily hot-swap template changes. However, the relocated assets directory made the code merge more complex.

Depending on your merging tool, the automated code merge may handle the relocated assets directory differently. We used svn merge. Unfortunately, svn merge was not smart enough to realize that the assets were relocated; it assumed that the assets directory was simply removed from its old location and that a new assets directory was added to its new location. As a result, the files were not merged at all. The new assets directory was the OOTB version.

 

Thus, we needed to manually merge all of our template changes. This process is not as bad as it sounds, since Elastic Path did not make any major changes to the templates from EP 6.0 to EP 6.1. We merged most of the templates by first diffing the EP 6.0 and EP 6.1 versions and then adding those differences to our customized versions.
Some of the bigger template changes made between EP 6.0 and EP 6.1 include:
  • Retrieving the store and catalog not from the ElasticPath object anymore but from the new StoreConfig object
  • Using the #emailMessage macro (instead of the #springMessage macro) in the email templates to display localized messages
Also worth mentioning is the new layout of the assets directory. Previously, all templates where put into a single templates directory. In EP 6.1, there is support for multiple themes and store-specific templates. For instance, the view-cart.vm template file may live in /assets/themes/mytheme/store1/templates/velocity/shoppingCart; this means that the store whose storecode is "store1" and whose theme is "mytheme" will use that particular view-cart.vm template. Another store, whose theme setting is set to "anothertheme"and doesn't use store-specific templates would have its view-cart.vm template in /assets/themes/anothertheme/default/templates/velocity/shoppingCart. During the merge, we made sure to set up the assets directory structure in a way that would best work for us.

Note: I mentioned above that template files can now be easily hot-swapped. One thing to keep in mind is that by default, templates are cached when the application starts up. If you hot-swap or modify a template file, you need to force a cache refresh by invalidating the cache. A quick and easy way to do this is to open the
invalidate-cache.ep URL in a web browser.

 


6. Setting up Maven

 

EP 6.1 uses Maven to manage project dependencies. In the past, if a project required a component (e.g. a 3rd party library) that was not used OOTB, we would put that component in the libs directory. With Maven, there's no longer a need for a libs directory, so we were able to remove it altogether during our merge. There was one caveat however: we were using a 3rd party library for GeoIP lookups (Maxmind). We added the dependency to our GeoIP library by adding the following in the storefront's pom.xml file:
<dependency>
    <groupId>maxmind</groupId>
    <artifactId>geoip</artifactId>
    <version>1.2.1</version>
    <scope>compile</scope>
</dependency>
The storefront is now dependant on the Maxmind GeoIP component. However, Maxmind is not available OOTB so we also had to make sure that Maven would be able to download the library files from somewhere.
Elastic Path uses Archiva repositories to store the dependent libraries. Rather than adding the Maxmind files to the internal Elastic Path repositories, we decided to add it to our team's Archiva repository. Archiva has a nice web interface to upload jar files into the repository. We added the following to elasticpath.pom to tell Maven that it needed to look in our team's archiva repository (maven-ps.elasticpath.net) for some of its dependencies:
<repository>
    <id>ps-extras</id>
    <name>Elastic Path PS Extras Repository</name>
    <url>http://maven-ps.elasticpath.net:8080/archiva/repository/extras/</url>
    <releases>
        <enabled>true</enabled>
    </releases>
    <snapshots>
        <enabled>false</enabled>
    </snapshots>
</repository>

To sum up, setting up Maven should require no work if your project does not have any custom dependencies. If your project does have dependencies that are not included with OOTB Elastic Path, they will need to be defined in the project object model (POM) files.

 


Testing & Bug Fixing


Our QA team's first task was to verify that EP 6.1 worked on our test servers. We grabbed the OOTB public release version of EP 6.1, deployed it on our test machines, and ran through a smoke test. Once we were able to confirm that the OOTB EP 6.1 worked on our servers, we focused on our merged codebase.

 

The code merge modified numerous files across the entire project. As a result, a full regression test on all of our customizations was required to verify that the code merge was done correctly. It goes without saying that the full regression test took a considerable amount of time to complete. The faster the code is merged, compiled cleanly, and deployed to test servers, the sooner testing can begin. After merging the code, we pushed a testable build out as soon as we could so that our QA team could begin testing. The first half-dozen builds contained many blocking bugs, but they were still testable. The key is to fix the blocking bugs quickly so that the QA team can be kept busy with the full regression test.

 

The JUnit tests should also be run and fixed as soon as possible, preferably even before the first testable build is deployed; they are a great way to catch bugs at the source code level.

 

The new FIT tests in EP 6.1, however, were a little more difficult to run. They require some setup and many may be broken after the code merge depending on the customizations. We skipped the FIT tests during the code merge and instead chose to run them after the upgrade.

 


Time Estimates

 

There's no definitive answer for how long an upgrade will take. It depends on the amount and type of customizations. For our client's upgrade, the entire process -- including scoping, upgrading, testing, and bug fixing -- took three months. One developer completed the upgrade (i.e. database updates, code merge, etc.), two QA analysts performed the testing, and three developers tackled the bugs found by the testers. 8% of our budget was spend on scoping and estimating, 60% was spend on the actual upgrade tasks and bug fixing, and 32% was spent on testing.

 

These numbers will vary from project to project. For example, the client's custom settings required additional custom work, which took approximately two weeks to design and implement. This task is something we developed specifically for this project only.

 


Conclusion

 

Upgrading Elastic Path is neither trivial or impossible. Elastic Path provides documentation on the various aspects of upgrading to Elastic Path 6.1 (database updates, settings migration, assets relocation, etc.), but the complete story is different for each project. The amount of work depends on the number and type of customizations that have been made. If you have any questions or comments, feel free to post them here.

 

0 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

Elastic Path 6.1 provides a new way to manage your application configuration. In the past, settings were stored in a variety of places throughout the system; some were in XML files, others were in properties files, and still others were stored in different tables in the database. Now, the most commonly used settings are stored in the database and can be configured using the Commerce Manager.

Custom settings can be migrated to use this framework with little code change and would be able to take advantage of Elastic Path 6.1 multi-store setting management features.

Setting Definitions

Setting definitions are stored in the database, in the TSETTINGDEFINITION table. A setting definition contains:

 

  • a path
  • a default value
  • a value type
  • the maximum number of overrides

 

The path is a unique identifier for the setting. For example COMMERCE/STORE/giftcertificatesEnabled path refers to the setting that sets whether gift certificates are enabled for a given store.

The default value is the fallback to use when a specific value hasn't been assigned to the setting.

 

The value type indicates the type of value stored in the setting (a string, a number, a URL, etc.).

 

The maximum number of overrides specifies the number of values that can be assigned to a setting. Some settings can have multiple values assigned to them. Others can only have one. For example, a store-specific setting would need to have different values for different stores, so the number of overrides would be very large or unlimited (-1). For a system-wide setting, there is generally only one override.

Setting Values

The TSETTINGVALUE table contains the values that have been assigned to settings. Each value has a context. For system settings, the context is empty. For store-specific values, the context contains a store code.

Setting Metadata

Setting metadata is stored in the TSETTINGMETADATA table. It can be used to store additional configuration information relating to the setting. A setting can have any number of metadata associated with it.

Setting metadata consists of a set of key/value pairs. In Elastic Path 6.1, there are two metadata key/value pairs that can be assigned to settings:

 

  • RefreshStrategy, which defines the setting's refresh strategy

 

  • availableToMarketing, which indicates whether the setting is displayed in the store Marketing tab.

 

Setting Refresh Strategies

The setting refresh strategies metadata provides fine-grained control over when setting changes are applied to the system. In the past, most setting changes required restarting the server. Now, when the setting change is applied depends on how you've configured its refresh strategy. Out of the box, Elastic Path 6.1 provides four refresh strategies:

 

  • application: settings that use the application refresh strategy are only applied after the application is restarted

 

  • session: settings that use the session refresh strategy do not change during the lifetime of the user's session. If an administrator changes the setting value, users will not see the change until their session cookie is removed or expires and a new session is created

 

  • interval: settings that use the interval refresh strategy are cached for a specified period of time. Users do not see changes until the cache expires

 

  • immediate: settings that use the immediate refresh strategy are updated immediately. As soon as the administrator changes a setting that uses this strategy, the change is applied everywhere

Creating a Setting Definition

New setting definitions can be added by executing SQL insert statements. For example, the following creates a definition identified by the path CUSTOM/WIDGETS/widgetType:

INSERT INTO TSETTINGDEFINITION(UIDPK, PATH, DEFAULT_VALUE, VALUE_TYPE, MAX_OVERRIDE_VALUES)
VALUES(10001, "CUSTOM/WIDGETS/widgetType", "flat", "String", 0, 1);

 

For your custom settings, you should use a naming convention, to avoid conflicts with other settings. Do not use the COMMERCE path prefix, which is reserved for Elastic Path settings.

Adding a Value for the Setting

Setting values can be added by using the Commerce Manager client application. For more information, see the Commerce Manager User Manual.

You can also use SQL insert statements. Remember that you don't need to create a setting value if there is a default value that comes from the setting definition.

Assuming you do want to override the setting definition for a specific context (store code for stores), you can create the value with the following SQL:

INSERT INTO TSETTINGVALUE(UIDPK, SETTING_DEFINITION_UID, CONTEXT, CONTEXT_VALUE)
 VALUES(1011, 10001, "SLRWORLD", "circle")

 

Note the value of SETTING_DEFINITION_UID should match the UIDPK for the related row in TSETTINGDEFINITION.

Note that multiple setting values can be defined on a setting definition if the setting definition's max_override_values field is set to -1.

 

Settings Framework Services

Elastic Path 6.1 provides a low level SettingsService class to manage and retrieve of setting definitions and values. To get a setting value for a system level setting with no context:

SettingsService settingsService = getElasticPath()
   getBean(ContextIdNames.SETTINGS_SERVICE);
final int numOfMinutes = Integer.valueOf(settingsService.getSettingValue(
   "COMMERCE/APPSPECIFIC/RCP/idleTimeForLock").getValue());

 

To get a setting value for a particular context:

settingsService.getSettingValue(path, store.getCode()).getValue();

 

If there is no setting value in TSETTINGVALUE for the specified context, the system will fall back to the default value in TSETTINGDEFINITION. For example, the default value of the COMMERCE/STORE/giftcertificatesEnabled setting is false, but a setting value for the context SNAPITUP is true, so the Snap It Up store has gift certificates enabled.

 

A SettingsReader service interface is provided for read only operations. Additionally, the CachedSettingsReader service wrapper provides caching for settings retrieval, and is the preferred method in the storefront to avoid frequent database calls.

 

In storefront controllers, the StoreConfig object (available through the RequestHelper) can be used to obtain setting values. If a store specific value is available, it will automatically be retrieved. Otherwise, the default value from the setting definition is returned.  For example:

String pagination = getRequestHelper().getStoreConfig()
 getSetting("COMMERCE/STORE/CATALOG/catalogViewPagination").getValue();
int paginationNumber = NumberUtils.toInt(pagination, DEFAULT_PAGINATION);

 

Elastic Path 6.1's settings framework should replace usage of configuration files for custom configuration settings, especially where multiple stores are being deployed. Previous code retrieving settings from the ElasticPathImpl object should be migrated to the settings framework and its suite of APIs.

0 Comments 0 References Permalink