Technical Blog

10 Posts tagged with the tagging_framework tag

Elastic Path Commerce version 6.2 was released in January with little fanfare, but don’t let that fool you; 6.2 is packed with lots of advanced ecommerce features. This release was all about giving merchants more flexibility in terms of how they sell their products. One of the big features we added was bundling (or kitting, if you prefer). Bundling gives merchants the ability to configure groups of products that can be sold as package deals. This gives their customers greater value and simplifies their purchase decisions. For merchants who wish to give their customers the value of package deals, but flexibility of choice, they can use dynamic bundles. Dynamic bundles give customers the choice between several merchant-defined options.

 

Many merchants will also be happy to learn that we've moved prices out of the catalog and into price lists. And by linking price lists to our targeted selling framework,  we’ve given merchants the ability to target price lists to different markets and customer segments. For B2B merchants, price lists are a great way to manage negotiated contract pricing for different accounts.

 

In 6.2, we've also introduced the ability to personalize products. By creating a configurable product type, merchants can give their customers the power to customize products before checkout. A good example would be a custom screening printing site that allows shoppers to upload the designs they want to print on their T-shirts.

 

For store managers and IT staff involved in store operations, the new staging to production feature will be tremendously useful. It allows changes to products, prices, promotions and marketing content to be previewed in a staging environment, and submitted for review and approval before being pushed over to the production environment.

 

For the tech folks, the 6.2 release includes support for new versions of various application servers, Java 6 support, and an upgrade to JPA 1.2.1. Storefront performance has also been improved with the addition of multi-level caching and other performance enhancements. 6.2 includes upgrade scripts, which should allow existing clients to upgrade quickly and make use of all the new features that 6.2 has to offer.

 

For more information, check out the 6.2.0 release notes and stay tuned for blog posts looking more in depth at some of these exciting, new features.

0 Comments Permalink

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

 

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

Architecture Overview.jpg

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

 

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

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

 

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

 

The steps we'll be running through include:

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

 

Setting Up the Underlying Tag Data
Adding Customer Profile Attributes

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

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

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

screen-capture-3.png

 

Adding Custom Tag Definitions and Values

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

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

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

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

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

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

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

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

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

 

Adding Localized Content

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

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

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

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

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

 

Adding Tags to the Tag Dictionaries

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

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

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

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

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

 

Configuring the School Pricing

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

 

schools.png

 

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

screen-capture-5.png

 

Creating our own Tagger

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

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

 

Testing the Tagger

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

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

 

Tying it all Together

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

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

 

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

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

 

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

screen-capture-6.png

 

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

 

Segment away!

0 Comments Permalink

Prices are moving out

Posted by Michael Vax Oct 20, 2009

The upcoming 6.2 release of Elastic Path will include some big changes to price management. Currently, prices are a property of the product and live inside the catalog. This means the only way to have multiple prices for the same product is to use virtual catalogs. That's fine if you want to have different prices in different stores, but it doesn't help if you need to have multiple prices for the same product in the same store.

     

In 6.2, prices are getting lives of their own. They're moving out of catalogs.

     

There are several business and technical drivers behind this change:

 

  • In many enterprises, different systems are used to manage pricing and catalog information. This means different update cycles and integration points.
  • It is not uncommon to distribute responsibilities for maintaining pricing and catalog data between different departments or people.
  • Online retailers want to have flexibility to price products differently for different customer segments. For example, you may want to offer better prices to registered users.
  • The ability to support multiple price lists is especially important in B2B environments where prices can be determined by the contract with each business customer.
  • By separating Price from Product, it would be possible to cache other product information in memory, which would significantly improve storefront performance.

     

In 6.2, all price information (price tiers, sale and list prices, etc.) is now stored in price lists. A price list contains prices in one currency only. If your store supports multiple currencies, you will need to create a separate price list for each currency.

     

    FromCatalogToPL.jpg

     

 

There will be a new way to manage prices in Commerce Manager. There will be a Price List Manager activity that provides a way to manage a set of product prices from price list's perspective. Users will still be able to use the Pricing tab in the Product Editor to manage prices for a product, but under the hood, the prices are actually stored in multiple price lists, not in the Catalog.

 

Commerce Manager permissions have been extended to separate responsibilities for managing catalog from managing price information.

     

Separation of prices from catalogs is also reflected in EP's import/export features. When exporting catalogs using the Import-Export tool, the exported catalog data will not include price information anymore. Price lists will be imported and exported using Price List Import / Export instead. And in the Commerce Manager, it will be possible to import price lists in CSV format as well.

     

In 6.2 you will be able to define any number of price lists and use them with more than one catalog across multiple stores. Let's see how this works.

     

A Commerce Manager user associates a Price List with a Catalog by creating a Price List  Assignment (PLA).

    PLA.jpg

     

     

Each PLA has a priority and, optionally, some targeted selling conditions that determine which shoppers are eligible for the price list. These conditions use the same tags that are used to determine which Dynamic Content shoppers see in the storefront. A Price List Assignment is very similar to a Dynamic Content Delivery. For more information, see the blog posts on the Tagging Framework and Dynamic Content.

 

Users can associate multiple Price Lists with a catalog by creating multiple Price List Assignments, each with different priorities and targeted selling conditions.

    PLAs.jpg

     

By evaluating PLA conditions against the shopper's tag set, EP builds a Price List Stack from Price Lists that meet the targeted selling conditions. Price Lists are arranged in the stack according to their priorities. To determine a product's price, the system goes through the Price Lists in the stack until it finds the first price list that contains a price for the product.This means that it is not required for a Price List to have prices for all products in the Catalog. If, for example, you have different people managing prices for different product categories, you can create separate price lists and assign them all to the same catalog.

PL Stack.jpg

Price management is a complex area, and required quite a bit of upfront design and refactoring many points of the application. Being able to leverage the Tagging Framework for Price List Assignment was a big help. A lot of customers are really looking forward to this, so it was definitely worth the effort. We have a couple other big features coming in 6.2 and I'll try to take a few moments to talk about them in the next couple weeks.

0 Comments Permalink

Rumba is the code name for Elastic Path Commerce 6.1.2, which was officially released to customers early last week. The development work on this release began in late April and finished in August. Dubbed a feature release, it has a bit of everything included in it.

 

The key focus of Rumba was rounding out the tagging framework and dynamic content functionality. The tagging framework was enhanced to include tag value types, which allow for easy definition of UI helpers for various tags, validation of tag values and localization among other things. The reusable generic condition builder underwent a major facelift, when we introduced an easy to follow UI, nested conditions and a combination of AND and OR operators, which will allow users to take their shopper segmentation to the next level. Shopper segmentation was further enhanced by the introduction of several new tagging events and corresponding tags such as cart subtotal and in store search terms. We also provided a sample GEO IP 3rdparty integration with Quova. Through the integration with Quova, we developed several highly useful GEO IP tags such as country code tag, state/province tag, top level domain tag etc. All of these tags will make GEO targeting a very appealing prospect for our clients.

 

During the second half of Rumba development, we focused on usability enhancements and client requested fixes to existing functionality. We added column sorting to frequently used areas of the CM Client such as Order and Product Searches. This usability work will allow Customer Service Reps, Catalog Managers and other CM Client users to be much more efficient in their day to day tasks. We also improved the save message prompts in CM Client to include additional information about the objects being saved, which will allow CM Client users to make faster and more informed decisions.

Another area of the system that experienced a facelift is the permissions structure. Previously, everyone with access to CM Client had read permission on everything and read, update and write permissions on explicitly assigned areas of the CM Client. In 6.1.2, we've implemented a restricted view access policy, so that users who are not assigned specific activities, catalogs, stores and warehouses will not have access to them at all. Finally, the tax calculations for inclusive tax jurisdictions were improved, by addressing bugs for edge cases. As a result, taxes are now covered by extensive automated tests, which will serve as a model of how we can better automate the testing of key aspects of our platform in the future.

 

There were also some technical improvements. We upgraded to Solr 1.3, and we are back on a mainline release. Previously we had customized Solr and Lucene to get it to do what we wanted. The new version now provides exactly what we need, so it's going to be easier to upgrade Solr and get bug fixes in the future. Solr 1.3 brings a lot of potential: more flexible config, better performance, runtime index creation/copying and more. Also, our build scripts are now leveraging antlion to provide build avoidance. Now, if the core engine is up to date and you rebuild storefront, you will only rebuild storefront, reducing overall build time considerably.

0 Comments Permalink

Give them an inch, they take a mile. Any time you give users the ability to enter free form text, there's a potential for something to go wrong. Thankfully, we've already got a variety of pieces in Elastic Path Commerce to validate user input. As of 6.1.2., the Not authorized to view the specified document 1262 includes a validation feature, so when users are creating Dynamic Content Delivery rules, the condition builder dialog can provide instant feedback and prevent them from submitting invalid values. To see this in action, fire up the CM client, go to the Store Marketing activity, and edit a Dynamic Content Delivery. When you get to the SHOPPERS screen (Step 4 of 6), add the "have a cart subtotal" tag. The tag requires a numeric value. If you try to enter alphabetical or special characters, the text field turns red. If you click the red x next to the field, you'll see an explanation of the error.

tag-validation.png

 

If you're creating your own tag definitions, you'll want to learn how to incorporate tag validation. Since I like to learn by doing, I gave it a try, and in this article, I'll walk you through the steps I followed to define a tag, capture values for it, and enable validation on it.

 

Before diving in, I should provide a very brief overview of how tag validation works. When you create a tag definition, you associate it with a tag value type. You can think of a tag value type as high-level data type. For example, in 6.1.2, there are over a dozen out-of-the-box tag value types, including time, age, gender, text, money, city, product category. Each tag value type has its own set of validation constraints, which determine whether a given value qualifies as a valid instance of that type. For example, the default validation constraints for the age tag value type require the value to be a number between 0 and 120. The validation constraints are used most notably in the Dynamic Content Delivery wizard, to ensure that users enter sensible values when creating SHOPPER segment conditions. Behind the scenes, we use Spring Validation (or Valang) to interpret the validation constraints. We'll look a bit more at this later on, since this is probably the most interesting part of it.

 

The first thing I needed to do was to decide what kind of tag I wanted to create. For the purpose of testing validation, I figured a phone number tag might be a good one to try. I created a tag value type in the database (identified by the GUID phone_number) and specified the operators it needed to support.

insert into TTAGVALUETYPE(uidpk, guid, java_type)
     values(17, 'phone_number', 'java.lang.String');

insert into TTAGVALUETYPEOPERATOR(tagvaluetype_guid, tagoperator_guid)
     values('phone_number', 'includes'),
          ('phone_number', 'greaterThan'),
         ('phone_number', 'lessThan'),
          ('phone_number', 'notIncludes');

Then, I created a basic validation constraint.

insert into TVALIDATIONCONSTRAINTS(uidpk, object_uid, error_message_key, validation_constraint, type)
     values(17, 17,     'validationTagPhoneNumberError', 
     '{ condition : isValidConditionType(this) is true AND match(\'[0-9]\\\\d{2}-\\\\d{3}-\\\\d{4}\', tagValue) is true : \'value must be a valid phone number (only numbers)\' }',
     'TagValueType');

Okay, that might look a bit scary. The object_uid and type columns together identify the tag value type (the uidpk in the TAGVALUETYPE table). The validation_constraint column contains the interesting part. The format of the validation constraint is as follows:

{ condition: <constraint> : <error message> }

The first part of the constraint, isValidConditionType(this), checks to make sure the value is not null, that its Java type matches the tag value type's Java type, and a few other checks. As a rule, you should always include this in your constraints.

 

The second part is where it gets interesting. If you remove some of the escape sequences, you're left with a condition that includes a simple regular expression to match North American telephone numbers.

match('[0-9]\\d{2}-\\d{3}-\\d{4}', tagValue) is true

match is a Valang function that returns true if the string in the second argument matches the regular expression in the first argument. The tagValue is a placeholder for the value that's being evaluated. There are a number of other functions available in Valang and you'll want to get familiar with them by reading the Valang documentation, but the syntax is pretty straightforward.

 

The last step for setting up the validation constraint is to create a localized error message for it.

insert into TLOCALIZEDPROPERTIES(uidpk, object_uid, localized_property_key, value, type)
     values(100174, 17, 'validationTagPhoneNumberError_en',
        'The phone number must match the format XXX-XXX-XXXX',
        'TagValueConstraint');

 

With this in place, I could define tags and set their tag value type to phone_number. To demonstrate that it worked, I created a customer telephone number tag, attached it to the SHOPPER tag dictionary, and then went into the Dynamic Content Delivery dialog to try it out.

invalid-phone-number.png

When I tried to enter letters or special characters other than the dash, the field background turns red and I could click the x to find out what I'd done wrong.

 

This is just a simple example. A quick look at the Valang documentation will reveal how flexible and powerful it is. I was also really surprised at how easy it was to set up a tag value type and associate it with validation constraints. In an upcoming article, I'll take things one step further and show how to replace the tag value's free-form text box with a drop-down list or combo box.

0 Comments Permalink

In 6.1.2, we added a slick condition builder widget to the Dynamic Content Delivery wizard. If you're thinking of using the Tagging Framework for your own purposes, you too can take advantage of this widget. In this article, we'll look at how to create a custom tag dictionary and a custom condition builder dialog to allow users to build conditions using the tags in a custom tag dictionary.

 

Before we get into coding, let's take a quick look at the new Dynamic Content Delivery dialog to see how the widget actually works.

 

How it works

Designing an intuitive UI for building conditions was a challenge for the design team. Most business users don't have a lot of experience with software applications that let them design complex rules for things like dynamic content and promotions. Usually, that falls in the domain of software programmers. So how do you give users almost code-level flexibility without completely scaring them away with its complexity? We banged our collective head against the wall for a long time over the course of multiple design sessions until finally we'd come up with something everyone was pleased with.

 

condbuilder_with_callouts1.png

As you can see, there are a lot of different parts here. The basic building block of a condition is a statement. A statement consists of

  • the tag
  • the tag operator
  • the tag value.

A condition must contain at least one statement. Creating a condition with a single statement is pretty straightforward. For example, assume you want to build a condition for shoppers who are 60 or over. You'd click add statement, expand the tag group, and select the tag, which in this case is "are of age". (Note that the "are of age" tag is located in the "Customer Profile" group. Tag groups were added in 6.1.2 as a way to organize tags in the condition builder.)

1add_age_tag.png

Next, you'd select the tag operator. In this case, we want "greater than or equal to".

2select_operator.png

Finally, you set a tag value of 60, which means you only want to match shoppers whose "are of age" tag contains a value that is greater than or equal to 60.

3enter_tag_value.png

That's a simple example, but we can create much more complicated conditions by combining multiple statements in a statement block.

 

A statement block can contain one or more statements. In the previous example, we had a default statement block containing a single statement. We can add additional statements by clicking the add statement link. The statements within a statement block are combined by either the AND or the OR operator. Only one operator can be used within a statement block. If you need to mix AND and OR, you can add additional statement blocks.

 

Creating the tag dictionary, tag group, and tags

 

First, we create a custom tag dictionary and some custom tags. Here's the SQL (mind the hard-coded primary keys).

/* Create a tag dictionary: */     
     insert into TTAGDICTIONARY(uidpk, guid, name, purpose)
          values(4, 'CUSTOM', 'Custom', 'Used to store custom tags and conditions');

/* Create a tag group: */
     insert into ttaggroup(uidpk, guid)
          values(5, 'CUSTOM_GROUP');


/* Create custom tags: */
     insert into TTAGDEFINITION(uidpk, guid, name, description, tagvaluetype_guid, taggroup_uid)
          values(22, 'CUSTOM_TAG_1', 'Custom 1', 'A custom tag', 'text', 5),
          (23, 'CUSTOM_TAG_2', 'Custom 2', 'A custom tag', 'text', 5),
          (24, 'CUSTOM_TAG_3', 'Custom 3', 'A custom tag', 'text', 5),
          (25, 'CUSTOM_TAG_4', 'Custom 4', 'A custom tag', 'text', 5);


/* Map custom tags to custom dictionary: */
     insert into TTAGDICTIONARYTAGDEFINITION(tagdictionary_guid, tagdefinition_guid)
          values('CUSTOM', 'CUSTOM_TAG_1'),
          ('CUSTOM', 'CUSTOM_TAG_2'),
          ('CUSTOM', 'CUSTOM_TAG_3'),
          ('CUSTOM', 'CUSTOM_TAG_4');


/* Create language resources for the tags and tag group: */
     insert into TLOCALIZEDPROPERTIES (UIDPK,LOCALIZED_PROPERTY_KEY,VALUE,TYPE,OBJECT_UID)
          values (50062,'tagDefinitionDisplayName_en','Custom tag 1','TagDefinition',22),
          (50063,'tagDefinitionDisplayName_en','Custom tag 2','TagDefinition',23),
          (50064,'tagDefinitionDisplayName_en','Custom tag 3','TagDefinition',24),
          (50065,'tagDefinitionDisplayName_en','Custom tag 4','TagDefinition',25),
          (50066,'tagGroupDisplayName_en','Custom group','TagGroup',5);

 

Creating the condition builder UI

Now, you want to create a CM client plugin with a dialog that will allow users to create and edit conditions using only the tags in this dictionary. I won't get into the details of creating a CM client plugin; you can look at the attached source code to see how I did it. The part we're interested in is the dialog that extends AbstractEpDialog. In my createEpDialogContent implementation, I add a text box to contain the name of the condition, a label for the text box, and the code that displays the condition builder composite.

protected void createEpDialogContent(final IEpLayoutComposite dialogComposite) {
          
     /* 
     * Set the color on the top level composite. This recursively sets the color of  
     * the condition builder's constituent controls.
     */     
     final Shell shell = this.getShell();
     final RGB rgb = new RGB(255, 255, 255);
     final Color color = new Color(shell.getDisplay(), rgb);           
     shell.setBackground(color);
     color.dispose();
                  
     // Add the condition name field label
     final IEpLayoutData labelData = dialogComposite.createLayoutData(IEpLayoutData.FILL, IEpLayoutData.FILL, true, true, 2, 1);
     dialogComposite.addLabelBoldRequired(MyConditionBuilderMessages.ConditionName, EpState.EDITABLE, labelData);
          
     // Add the condition name field
     final IEpLayoutData fieldData = dialogComposite.createLayoutData(IEpLayoutData.FILL, IEpLayoutData.FILL, true, false, 2, 1);
     conditionNameText = dialogComposite.addTextField(EpState.EDITABLE, fieldData);
     conditionNameText.setTextLimit(CONDITION_NAME_TEXT_LIMIT);
 
     // Add the condition builder composite
     final Composite composite = displayConditionBuilder(shell);
     composite.setParent(dialogComposite.getSwtComposite());
}

 

The actual implementation of displayConditionBuilder is where the real work happens.

private Composite displayConditionBuilder(final Composite parentComposite) {
     
     ElasticPath elasticPath = Application.getInstance().getElasticPath();
 
     // get the tag operator service
     TagOperatorService tagOpService = (TagOperatorService)
               elasticPath.getBean(ContextIdNames.TAG_OPERATOR_SERVICE);
     
     // get tag definitions from the custom dictionary
     TagDictionaryService dictionaryService = (TagDictionaryService)
               elasticPath.getBean(ContextIdNames.TAG_DICTIONARY_SERVICE);
     TagDictionary tagDictionary = dictionaryService.findByGuid(CUSTOM_DICTIONARY);
     Set<TagDefinition> tagDefinitions = tagDictionary.getTagDefinitions();
     
     // get the tag groups for those tag definitions
     TagGroupService tagGroupService = (TagGroupService)
               elasticPath.getBean(ContextIdNames.TAG_GROUP_SERVICE);                    
     Set<TagGroup> tagGroups = new HashSet<TagGroup>();          
     for (TagDefinition tagDefinition : tagDefinitions) {
          if (tagDefinition.getGroup() == null) { continue; }
          tagGroups.add(tagGroupService.findByGuid(tagDefinition.getGroup().getGuid()));
     }
     
     ConditionBuilderFactoryImpl factory = new ConditionBuilderFactoryImpl();
     factory.setDataBindingContext(this.dataBindingCtx);
     factory.setLocale(CorePlugin.getDefault().getDefaultLocale());
     factory.setTagOperatorService(tagOpService);
     factory.setConditionBuilderTitle(""); //$NON-NLS-1$
     factory.setTagGroupsList(new ArrayList<TagGroup>(tagGroups));
     
     factory.getResourceAdapterFactory().setResourceAdapterForLogicalOperator(
               new ResourceAdapter<LogicalOperatorType>() {
                    public String getLocalizedResource(final LogicalOperatorType object) {
                         return AdminGCMessages.getMessage(object.getMessageKey());
                    } });
 
     factory.getResourceAdapterFactory().setResourceAdapterForUiElements(
               new ResourceAdapter<String>() {
                    public String getLocalizedResource(final String object) {
                         return AdminGCMessages.getMessage(object);
                    } });
 
     factory.setListenerForRefreshParentComposite(
               new ActionEventListener<Object>() {
                    public void onEvent(final Object object) {
                         parentComposite.pack();
                         parentComposite.layout();
                    } });
     
     Composite composite = factory.createFullUiFromModel(parentComposite, SWT.FLAT, this.logicalOperator);
     return composite;
}

That looks like a lot of code, but we'll break it down by sections so we can make more sense of it.

 

In the first section, we get references to some of the tag-related services. The TagOperatorService is used internally by the condition builder factory.

     TagOperatorService tagOpService = (TagOperatorService)
               elasticPath.getBean(ContextIdNames.TAG_OPERATOR_SERVICE);

Next, we use the TagDictionaryService to get the tags from the custom dictionary.

     TagDictionaryService dictionaryService = (TagDictionaryService)
               elasticPath.getBean(ContextIdNames.TAG_DICTIONARY_SERVICE);
     TagDictionary tagDictionary = dictionaryService.findByGuid(CUSTOM_DICTIONARY);
     Set<TagDefinition> tagDefinitions = tagDictionary.getTagDefinitions();

 

Then, we use the TagGroupService to get the groups associated with those tags. In order to include a tag in the condition builder, it must be associated with a group.

     TagGroupService tagGroupService = (TagGroupService)
               elasticPath.getBean(ContextIdNames.TAG_GROUP_SERVICE);                    
     Set<TagGroup> tagGroups = new HashSet<TagGroup>();          
     for (TagDefinition tagDefinition : tagDefinitions) {
          if (tagDefinition.getGroup() == null) { continue; }
          tagGroups.add(tagGroupService.findByGuid(tagDefinition.getGroup().getGuid()));
     }

The next step is to instantiate the condition builder factory and configure it with the TagOperatorService and the list of tag groups.

// configure the condition builder factory
ConditionBuilderFactoryImpl factory = new ConditionBuilderFactoryImpl();
factory.setDataBindingContext(this.dataBindingCtx);
factory.setLocale(CorePlugin.getDefault().getDefaultLocale());
factory.setTagOperatorService(tagOpService);
factory.setConditionBuilderTitle(""); //$NON-NLS-1$
factory.setTagGroupsList(new ArrayList<TagGroup>(tagGroups));

We need to specify how the condition builder gets the language strings for its UI elements and labels. We do this by configuring generic resource adapters on the condition builder factory.

factory.getResourceAdapterFactory().setResourceAdapterForLogicalOperator(
          new ResourceAdapter<LogicalOperatorType>() {
               public String getLocalizedResource(final LogicalOperatorType object) {
                    return AdminGCMessages.getMessage(object.getMessageKey());
               } });
 
// configure the resource adapter that provides the UI labels for the other UI components
factory.getResourceAdapterFactory().setResourceAdapterForUiElements(
          new ResourceAdapter<String>() {
               public String getLocalizedResource(final String object) {
                    return AdminGCMessages.getMessage(object);
               } });

The resource adapters delegate to the AdminGCMessages utility class, which defines constants for the language resources. The actual language strings are stored in com.elasticpath.cmclient.admin.gc.AdminGCPluginResources.properties.

public final class AdminGCMessages {
 
     /* Property file binding. */
     private static final String BUNDLE_NAME = "com.elasticpath.cmclient.admin.gc.AdminGCPluginResources"; //$NON-NLS-1$
 
...
 
     /* Generic condition builder UI Labels */
     public static String LogicalOperator_AND;
     public static String LogicalOperator_OR;
     
     public static String ConditionBuilder_Title;
     public static String ConditionBuilder_AddConditionButton;
     
     public static String ConditionBuilder_Add_Rule_label;
     public static String ConditionBuilder_Remove_Rule_label;
 
     
     static {
          NLS.initializeMessages(BUNDLE_NAME, AdminGCMessages.class);
     }
     
...
     public static String getMessage(final String messageKey) {
          try {
               final Field field = AdminGCMessages.class.getField(messageKey);
               return (String) field.get(null);
               
          } catch (final Exception e) {
               return messageKey;
          }
     }     
}

We also need to configure a listener to ensure that the parent composite gets resized whenever statements and statement blocks are added or removed from the widget.

factory.setListenerForRefreshParentComposite(
          new ActionEventListener<Object>() {
               public void onEvent(final Object object) {
                    parentComposite.pack();
                    parentComposite.layout();
               } });

The last step is to call the factory's createFullUiFromModel method and return the Composite that contains the condition builder.

Composite composite = factory.createFullUiFromModel(parentComposite, SWT.FLAT, this.logicalOperator);
return composite;

The first argument to createFullUiFromModel  is the Composite object that will contain the condition builder. The third argument is a LogicalOperator object that represents the condition model.

 

Note that conditions are persisted as ConditionalExpression objects. If you look at the attached source code for the dialog, you'll see that the dialog constructor takes a ConditionalExpression object and creates a LogicalOperator from it.

public GCDialog(final Shell parentShell, final ConditionalExpression expression, final String title, final Image image) {
     super(parentShell, 2, false);
     this.title = title;
     this.titleImage = image;
     this.expression = expression;
     this.dataBindingCtx = new DataBindingContext();
     
     // convert the expression to a DSL string and populate the logical operator
     ConditionDSLBuilder dslBuilder = (ConditionDSLBuilder) 
               Application.getInstance().getElasticPath().getBean(ContextIdNames.TAG_CONDITION_DSL_BUILDER);
               
     String conditionString = this.expression.getConditionString();
     
     if (conditionString == null) {
          this.logicalOperator = new LogicalOperator(LogicalOperatorType.AND);
          this.logicalOperator.addLogicalOperator(new LogicalOperator(LogicalOperatorType.OR));                    
     } else {
          this.logicalOperator = dslBuilder.getLogicalOperationTree(conditionString);
     }          
                              
     setAuthorized();                    
}

A ConditionalExpression object is passed in by the action that creates the dialog. In the constructor, we use the expression's getConditionString method to return the condition as a string value. (You'll see some neat Groovy code if you take a look at it.) Then, we use the ConditionDSLBuilder service's getLogicalOperationTree method to convert that string to a LogicalOperator object.

 

To save changes made by the user, we convert the LogicalOperator back to a condition string and update the ConditionalExpression object. You can see this in the okPressed method of the dialog:

protected void okPressed() {
     dataBindingCtx.updateModels();          
     
     ElasticPath elasticPath = Application.getInstance().getElasticPath();
     
     // convert the logical operator to a condition string so it can be persisted
     ConditionDSLBuilder dslBuilder = elasticPath.getBean(ContextIdNames.TAG_CONDITION_DSL_BUILDER);               
     String conditionString;
     try {
          conditionString = dslBuilder.getConditionalDSLString(this.logicalOperator);
          this.expression.setConditionString(conditionString);
          
          // set the condition name
          this.expression.setName(this.conditionNameText.getText());
          
          // set the dictionary name
          this.expression.setTagDictionaryGuid(CUSTOM_DICTIONARY);
 
          // make sure it's saved as a named condition
          this.expression.setNamed(true);
          
          super.okPressed();
          
     } catch (InvalidConditionTreeException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
     }
}

 

The actual persistence code is in the Action code that opened the dialog (the EditGCAction and CreateGCAction classes):

public void run() {
     ElasticPath elasticPath = Application.getInstance().getElasticPath();  
     ConditionalExpression expression = (ConditionalExpression) elasticPath.getBean(ContextIdNames.CONDITIONAL_EXPRESSION);                    
     boolean dialogOk = GCDialog.openCreateDialog(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(), 
               expression);
          
     if (dialogOk) {               
          TagConditionService conditionService = (TagConditionService) elasticPath.getBean(ContextIdNames.TAG_CONDITION_SERVICE);          
          conditionService.saveOrUpdate(expression);          
          listView.refreshViewerInput();               
     }
}

There's definitely a lot going on in several different places and I've tried to touch as many of the key areas as possible. Hopefully, the attached code will answer some of the outstanding questions, but I'm sure there are others. Please post any questions you have here.

0 Comments Permalink

6.1.2 brings bug fixes and some enhancements to the Elastic Path Commerce platform, mainly in the areas of the Tagging Framework and CM client usability (list sorting and searching). At a glance:

  • Geo IP tags: 6.1.1 introduced Dynamic Contentand the Tagging Framework, which gave marketers the ability to personalize the shopping experience based on who the customer is (age, referring site, search engine terms). With the addition of Geo IP tags in 6.1.2, it's now possible to target content based on the shopper's geographic location (city, state, country, region, etc.).
  • Visited categories tag: a new tag in the Tagging Framework that keeps track of the categories that the the shopper has visited.
  • Cart subtotal tag: tracks the shopping cart subtotal.
  • In-site search terms tag: contains the search terms entered in the storefront's search box.
  • Generic condition builder: 6.1.1 included a simple UI for building Dynamic Content Delivery conditions. 6.1.2 expands on that with a more flexible, intuitive UI. (If you've worked with Google Analytics' advanced segments feature, it should look familiar.) This new UI was developed as an RCP UI widget, so it can easily be leveraged for other scenarios that require a condition builder to configure tag evaluation.
  • Tag value types and validation constraints: you can now configure critieria for validating the tag values entered in the Dynamic Content Delivery dialog.
  • Selectable values for tags: you can use drop-down lists to set tag values in the Dynamic Content Delivery dialog.
  • Saved conditions: it is now possible to create Dynamic Content Delivery conditions and save them for use in other Dynamic Content Deliveries.
  • Tag groups: tags can now be assigned to tag groups, to provide better visual organization in the Dynamic Content Delivery dialog.
  • Tag localization: tags, tag groups, and validation constraints now support localization through the TLOCALIZEDPROPERTIES table.

 

There'll be some blog posts to go in depth on some of these topics, so keep watching this space.

1 Comments Permalink

In a recent post on the Get Elastic blog, Linda talked about personalizing content based on the customer's sorting behavior. For example, price-conscious shoppers might sort categories and search results by price from lowest to highest, so you want to show them promotional banners that highlight inexpensive or discounted items. Or maybe you have shoppers who place a higher value on popularity. These shoppers tend to sort by top sellers, and you want to target specific content to them.

 

But are there any ecommerce tools that can do this type of personalization out of the box? None that I know of. However, Elastic Path Commerce 6.1.1 has two new features that give us almost everything we need. In this article, we'll look at how to add sort-by personalization. And it's really quite easy, I promise.

 

Before we get started, be sure to read my previous posts on Dynamic Content and the Tagging Framework (Dynamic Content 101, Tagging Framework 101, and Targeting Dynamic Content for Mobile Device Users). For more in-depth technical information, you can check out the 6.1.1 Developer Guide or see the Commerce Manager User Guide for the marketing user perspective.

The Solution

When a visitor to the storefront selects one of the sorting options from the drop-down list, a request is sent to Elastic Path with a sorter parameter in the URL. For example, if the shopper is browsing a category and sorts by price from lowest to highest, the URL will look like the following:

 

http://www.elasticpath.com/storefront/browse.ep?cID=100009&filters=c90000003&sorter=price-asc

 

The sorter parameter indicates which sorting option was selected by the shopper. Elastic Path 6.1.1 includes a TARGET_URL tag, which contains the query string, but this value is only captured at the time the shopper's session is created. So, we'll need to do a few things:

  • Create a QUERY_STRING tag and map it to the "who" tag dictionary.

  • Create a tag event listener interface and a tagger implementation to put the query string in a tag and add it to the shopper's tag set.

  • Create a filter to intercept the HTTP request, invoke the tag event listener, and pass it the request and customer session information.

  • Update the storefront request filter chain to include the new filter.

Once this is done, marketing users will be able to configure Dynamic Content Delivery to display specific content based on whether the QUERY_STRING tag includes the sorter parameter and what value it contains.

Create the QUERY_STRING Tag

To create the QUERY_STRING tag, you'll need to add it to the ttagdefinition table in your database:

 

insert into ttagdefinition(uidpk, guid, name, description, data_type)
    values(10, 'QUERY_STRING', 'QUERY_STRING',
    'The parameters in the request URL.', 'java.lang.String');

 

Next, you need to add the new tag to the "who" tag library:

 

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

 

Now, marketing can use the QUERY_STRING tag when they're building rules in the Commerce Manager for displaying Dynamic Content. However, we still need to tell Elastic Path how to get the query string and put it in the shopper's tag set. That's coming next...

Create the Tag Event Listener and Tagger

The Tagging Framework uses a lightweight event listener model. It doesn't require you to implement any particular interfaces, but it's a good idea from a design perspective. There are two tag event listener interfaces defined in the Elastic Path Commerce core library, under com.elasticpath.commons.listeners:

  • NewHttpSessionEventListener, which receives notifications when the shopper arrives at the storefront.

  • CustomerLoginEventListener, which receives notifications when the shopper signs in to their store account.

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

 

For this customization, we'll follow the model of these existing listeners. Because we need to listen for all HTTP requests, not just when it's a new session or when the customer logs in, you'll create an interface named HttpRequestEventListener. Like the other tag event listener interfaces, it only needs an execute method that takes the customer session and HTTP request objects as parameters:

 

public interface HttpRequestEventListener {
    public void execute(final CustomerSession session, final HttpServletRequest request);
}

 

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 need to create our own tagger class that implements the new interface, takes the query string from the request, and puts it in the QUERY_STRING tag in the customer's tag set:

 

public class QueryStringTagger implements HttpRequestEventListener {
    private static final String QUERY_STRING = "QUERY_STRING";
    private static final Logger LOG = Logger.getLogger(QueryStringTagger.class);    

    public void execute(final CustomerSession session,
final HttpServletRequest request) {
        String queryString = request.getQueryString();
        if (LOG.isDebugEnabled()) {
            LOG.debug("Populating customer session with the query string: " + queryString);

        }
        TagSet tagSet = session.getCustomerTagSet();
        tagSet.addTag(QUERY_STRING, new Tag(queryString));
    }
}

 

Keep in mind that the QueryStringTagger is going to get called on every request, so you want to avoid doing too much processing in here. The key things to note:

You get the tag set from the CustomerSession object by calling getCustomerTagSet.

  • You create a tag and set its value by calling the Tag constructor and passing it the value you want to assign.

  • You use the addTag method on the TagSet object to add the tag to the shopper's tag set.

We now have a tagger class that can receive notification of HTTP requests and put the query string in the shopper's tag set. The next step is to create the filter that will notify the tagger class and pass it the customer session and HTTP request information.

Create the Filter

We'll create the QueryStringFilter in the storefront web app in the com.elasticpath.sfweb.filters package. The filter needs to include a HttpRequestEventListener collection. We'll define the filter and the tagger bean definitions in the storefront web app's filter-config.xml:

 

<bean id="queryStringFilter" class="com.elasticpath.sfweb.filters.QueryStringFilter">

<property name="requestHelper">

<ref bean="requestHelper" />

</property>

<property name="httpRequestEventListeners">

<list>

<ref bean="queryStringTagger"/>

</list>

</property>

</bean>

<bean id="queryStringTagger" class="com.elasticpath.sfweb.listeners.QueryStringTagger"/>

 

Note the QueryStringTagger object in the httpRequestEventListeners collection. In the QueryStringFilter's doFilter method, we'll call the execute method of each event listener in the collection:

 

public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain filterChain)

throws IOException, ServletException {

 

   if (!(request instanceof HttpServletRequest)) {
       filterChain.doFilter(request, response);
       return;
    }

 

    HttpServletRequest httpServletRequest = (HttpServletRequest) request;

    CustomerSession customerSession = getCustomerSession(httpServletRequest);

 

    // Notify the request event listeners

    Collection<HttpRequestEventListener> listeners = getHttpRequestEventListeners();

    for (HttpRequestEventListener listener : listeners) {

        listener.execute(customerSession, httpServletRequest);

    }

    filterChain.doFilter(request, response);

}

 

CustomerSession getCustomerSession(final HttpServletRequest request) {

     return requestHelper.getShoppingCart(request).getCustomerSession();

}

Update the Filter Chain

Finally, we need to add the new filter to the filter chain in acegi.xml.vm and acegi.xml:

 

            <value>

                CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON

                \A/google-callback.ep.*\Z=channelProcessingFilter, basicProcessingFilter, basicExceptionTranslationFilter, basicFilterInvocationInterceptor

                \A/.*\Z=channelProcessingFilter, httpSessionContextIntegrationFilter, logoutFilter, logoutCustomerSessionFilter, onePageLogoutFilter, authenticationProcessingFilter, queryStringFilter, exceptionTranslationFilter, filterInvocationInterceptor

            </value>

 

Note the location of the queryStringFilter. Make sure you add it to the chain at some point after the customer session has been created, which generally occurs in the authenticationProcessingFilter.

 

Now that we've created the QUERY_STRING tag and the query string is getting stored in each visitor's tag set, it's time to let the marketing team test it out.

 

Using the example mentioned earlier, if you have a piece of Dynamic Content that you want to display  when shoppers sort a category or search results on price from lowest to highest, you can add a condition in the Dynamic Content Delivery like this:

dcd_query_string_tag_circled.png

Now, assuming you've got the proper Content Spaces in your category and search Velocity templates, the Dynamic Content will appear whenever your visitors sort by price from lowest to highest. And you can easily add others for different sorting habits, such as top sellers.

 

As you can see, a lot can be accomplished with just a small amount of initial coding. Be sure to download the attached source code and try it out for yourself.

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

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