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 Tagging Framework 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