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.

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.)

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

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.

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.