Technical Blog

3 Posts tagged with the condition_builder tag

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

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