Technical Blog

7 Posts tagged with the cm_client tag

Anyone who has had the pleasure of customizing the Commerce Manager knows that it is a complicated piece of software with a lot of moving parts. 

 

However, with the release of 6.2.1 combined with the new binary-based architecture and a number of clients going through an upgrade process, it has become even more complex with the introduction of patch fragments and a new approach to customizations.

 

For this blog post, I wanted to demonstrate a handy Eclipse plug-in that lets you visualize the dependencies between the various plug-ins within the Commerce Manager.  This is really useful for quickly discovering which plug-ins/bundles are dependent on each other.

 

The plug-in is called "Plug-In Dependency Graph Plugin". You can download it from http://testdrivenguy.blogspot.com/2009/05/eclipse-plug-in-dependency-graph.html. Then just unpack it into your Eclipse plugins folder and restart Eclipse.

 

Open the "Graph Plug-in Dependencies" view, click the search icon in the top right corner, and type in the name of the plugin you want to check out. In this example, I am interested in the our customized admin.configuration plugin.

 

admin-config-callees.png

 

You can see that there are a lot of dependencies between all the plugins, but by highlighting the admin.configuration plugin I can quickly see which plugins are directly referenced by it.

 

Alternatively, if you want to see who is directly referencing your plugin, you can show the callers:

 

admin-config-callers.png

In this case, not too interesting.  Let's take a look at com.elasticpath.cmclient.core:

 

cmclient-core-callers.png

 

A little more interesting. This plugin can help you quickly track down configuration issues in your plug-in manifest files when developing using the new patch fragment approach.

 

In future posts, I will provide more technical details as to our approach in implementing the patch fragment architecture including technical challenges encountered and our solutions to these issues.

0 Comments Permalink

As you may know, Eclipse RCP (which the CM Client is implemented on) is itself built upon the OSGI and runs in the Equinox OSGI container. That means there's the full power of the OSGI framework at your disposal.

 

One of the interesting features that Equinox provides is a console that allows you to poke around at the insides of a running OSGI application.

 

To see the console simply add the command line flag -console when launching an Eclipse application. The following is an example of accessing the console running Eclipse itself on my Mac, but you could do exactly the same when running the Commerce Manager.exe on Windows.

 

 

ep-wl-0594:eclipse35 ivanjensen$ ./eclipse/Eclipse.app/Contents/MacOS/eclipse -console

 

osgi>

 

 

You can see the osgi> prompt just there. There's lots of options in there that can be very useful in debugging issues. You can see all the available options by simply typing help at the osgi prompt:

 

 

osgi> help

---Controlling the OSGi framework---

        launch - start the OSGi Framework

        shutdown - shutdown the OSGi Framework

        close - shutdown and exit

        exit - exit immediately (System.exit)

        init - uninstall all bundles

        setprop <key>=<value> - set the OSGi property

---Controlling Bundles---

        install - install and optionally start bundle from the given URL

        uninstall - uninstall the specified bundle(s)

        start - start the specified bundle(s)

        stop - stop the specified bundle(s)

        refresh - refresh the packages of the specified bundles

        update - update the specified bundle(s)

---Displaying Status---

        status [-s [<comma separated list of bundle states>]  [<segment of bsn>]] - display installed bundles and registered services

        ss [-s [<comma separated list of bundle states>]  [<segment of bsn>]] - display installed bundles (short status)

        services [filter] - display registered service details

        packages [<pkgname>|<id>|<location>] - display imported/exported package details

        bundles [-s [<comma separated list of bundle states>]  [<segment of bsn>]] - display details for all installed bundles

        bundle (<id>|<location>) - display details for the specified bundle(s)

        headers (<id>|<location>) - print bundle headers

        log (<id>|<location>) - display log entries

---Extras---

        exec <command> - execute a command in a separate process and wait

        fork <command> - execute a command in a separate process

        gc - perform a garbage collection

        getprop  [ name ] - displays the system properties with the given name, or all of them.

 

...output abbreviated...

 

osgi>

 

 

The command I use most frequently to get a general overview of which bundles are available and their status is ss:

 

 

osgi> ss     

 

Framework is launched.

 

id      State       Bundle

0       ACTIVE      org.eclipse.osgi_3.5.0.v20090520

1       ACTIVE      org.eclipse.equinox.simpleconfigurator_1.0.100.v20090520-1905

2       <<LAZY>>    com.ibm.icu_4.0.1.v20090415

3       RESOLVED    com.jcraft.jsch_0.1.41.v200903070017

4       <<LAZY>>    com.sun.jna_3.1.0

5       RESOLVED    java_cup.runtime_0.10.0.v200803061811

6       RESOLVED    javax.activation_1.1.0.v200905021805

7       RESOLVED    javax.mail_1.4.0.v200905040518

8       RESOLVED    javax.servlet_2.5.0.v200806031605

9       RESOLVED    javax.servlet.jsp_2.0.0.v200806031607

 

...output abbreviated...

 

osgi>

 

 

Here you can see some of the bundles that run in my installation of eclipse. Each bundle is given a unique number that can be used with some of the other console commands. You can also see the bundle's state and the bundle's symbolic name. For more information on the bundle states check out this Wikipedia article.

 

There's plenty more good stuff down in the console, including being able to start, stop and update bundles in a running system.

 

So why not take some time and get to know your OSGI console and add another string to your RCP bow?

0 Comments Permalink

This post walks through adding filtering to the System Configuration page in the CM Client helping you track down a setting as quickly as possible.

 

We added the Settings Framework in Elastic Path Commerce 6.1 and it's proved incredibly useful in centralizing configuration and easing customization versus the previous XML based approach.  The number of settings out-of-the-box continues to grow plus any additional ones that customers are free to add.  So this tip will show you how to reign in that growing number of settings.

 

Here's a screenshot of what we'll be producing, if you've spent any time in the System Configuration (like our trusty QA guys) this will be a real time saver.

 

settings-filter.png

 

Changing the layout

I had to refresh my SWT widget layout knowledge a little and found this great page: http://www.eclipse.org/articles/article.php?file=Article-Understanding-Layouts/index.html.  With that excellent refresher I decided I would need a three column GridLayout: a column each for the Edit button, filter label and the filter text widget.  The table would then span all three columns.

 

settings-filter-layout.png

First, simply change the number of columns on the SettingDefinitionComposite:

 

private void setupLayout() {
     final int columns = 3;
     this.setLayout(new GridLayout(columns, false));
}

 

Then add in a horizontal span to the table's LayoutData - this makes it span the three columns we just created.

final int horizontalSpan = 3;
table.setLayoutData(new GridData(GridData.FILL, GridData.FILL, true, true, horizontalSpan, 1));

Note: the final variables are to keep Checkstyle quiet about magic numbers.

 

Adding the new widgets

The code below adds the label and the filter Text widget.  Take a close look at the style we are using to create the Text widget with: SWT.SEARCH | SWT.CANCEL.  The SWT.SEARCH style gives us the rounded edges (at least on my Mac) which makes it look like a regular search/filter widget.  The SWT.CANCEL style adds the small cross to the widget.  When clicked it removes the currently entered text.

Label label = formToolkit.createLabel(this, AdminConfigurationMessages.filterLabel + ":"); //$NON-NLS-1$
GridData gridData = new GridData(SWT.END, SWT.CENTER, false, false);
label.setLayoutData(gridData);
Text settingNameFilter = formToolkit.createText(this, "", SWT.SEARCH | SWT.CANCEL); //$NON-NLS-1$
gridData = new GridData(SWT.FILL, SWT.CENTER, false, false);
final int verticalIndent = 10;
gridData.verticalIndent = verticalIndent;
settingNameFilter.setLayoutData(gridData);

 

We want this new addition to be localizable so we need to add AdminConfigurationMessages.filterLabel

public static String filterLabel;

 

And then we provide the English version of that in AdminConfigurationPluginResources.properties

 

filterLabel=Filter

 

Filtering

Now we'll take a look at how we will actually filter down the table's contents.  The code's pretty simple:

/**
 * Filter setting definitions against a specified string.
 */
private class SettingPathFilter extends ViewerFilter {
     private final String filterText;
     public SettingPathFilter(final String filterText) {
          this.filterText = filterText;
     }
     @Override
     public boolean select(final Viewer viewer, final Object parent, final Object element) {
          SettingDefinition definition = (SettingDefinition) element;
          return StringUtils.containsIgnoreCase(definition.getPath(), filterText);
     }
}

We've simply extended the JFace ViewerFilter class and implemented the select method with a case-insensitive check against the setting definition's path.  In the next step we'll create an instance of this class and pass it to the TableViewer that holds the setting definitions.

 

The following page helped me a bit with the ViewerFilter: http://www.java2s.com/Code/Java/SWT-JFace-Eclipse/DemonstratesListViewer.htm

 

Hooking it together

The bits are all in place, let's tie them together and get the filtering going when a user types in the text box.  We simply add a ModifyListener to the filter Text widget and when we receive a ModifyEvent we grab the user-entered string, create a new SettingPathFilter and set that on the tableViewer.  That triggers the tableViewer to filter its contents.  Note: I chose to use setFilters, rather than addFilter/removeFilter, to keep the code simple: I don't have to keep track of the filter to remove it afterwards I simply call setFilters again.

 

settingNameFilter.addModifyListener(new ModifyListener() {
     public void modifyText(final ModifyEvent event) {
          final Text source = (Text) event.getSource();
          String filterText = source.getText();
          if (StringUtils.isBlank(filterText)) {
               tableViewer.setFilters(new ViewerFilter [0]);
          } else {
               tableViewer.setFilters(new ViewerFilter [] {new SettingPathFilter(filterText)});
               }
     }
});

 

 

Conclusion

So there you have it, all done, with a few small changes we can find settings without having to visually scan the table.  Where else might this filtering function be useful?  Anyone done any similar customizations they would like to share?

 

The code

The code changes and the attached patch are against the upcoming 6.2 release, but I'm pretty sure they will apply to any 6.1+ version.  Leave a comment if you have any problems applying this and I'll do my best to help you out.

References

http://www.eclipse.org/swt/snippets/ - if you've not taken a look at the SWT snippets then you're missing out.  There's a ton of useful examples of using SWT.

http://wiki.eclipse.org/index.php/JFaceSnippets - just like the SWT snippets, this page contains great snippets about using the JFace ui toolkit.

2 Comments Permalink

Elastic Path comes with a very flexible attribute system, allowing you to customize the attributes associated with products, skus, customers, categories, and more. Attributes can be expressed as several different types -- strings, lists of strings, integers, decimals, booleans, images, etc. -- so that a great variety of information can be associated with your domain objects.

 

Editing this information is fairly straightforward in the Commerce Manager client. However, if you'd like to restrict an attribute to certain values, there is currently no way to enforce this in the client, and the potential exists for entering data incorrectly. This can cause problems if you are relying on the value of a particular attribute for functionality on your website, or perhaps for interfacing with your back-end systems.

 

Fortunately, we have the EP source code and can implement this functionality with a little effort, using a list of options stored in the database and some modifications to the Commerce Manager Client.

 

Domain Objects

 

Attribute values are derived from com.elasticpath.domain.attribute.impl.AbstractAttributeValueImpl, which is a "mapped superclass", meaning that there is no superclass db table and subclasses exist separately in their own db table. Since we need to maintain a per-attribute list of legal attribute values, we can simply leverage this existing infrastructure:

 

@Entity
@Table(name = AttributeOptionImpl.TABLE_NAME)
public class AttributeOptionImpl extends AbstractAttributeValueImpl {
 
     /**
      * The name of the table & generator to use for persistence.
      */
     public static final String TABLE_NAME = "TATTRIBUTEOPTION";
     
     private long uidPk;
 
     /**
      * Gets the unique identifier for this domain model object.
      *
      * @return the unique identifier.
      */
     @Id
     @Column(name = "UIDPK")
     @GeneratedValue(strategy = GenerationType.TABLE, generator = TABLE_NAME)
     @TableGenerator(name = TABLE_NAME, table = "JPA_GENERATED_KEYS", pkColumnName = "ID", valueColumnName = "LAST_VALUE", pkColumnValue = TABLE_NAME)
     public long getUidPk() {
          return uidPk;
     }
 
     /**
      * Sets the unique identifier for this domain model object.
      *
      * @param uidPk the new unique identifier.
      */
     public void setUidPk(final long uidPk) {
          this.uidPk = uidPk;
     }
}

 

Because the superclass already defines the link to the Attribute as well as the fields for storing the actual value, there is nothing more to define in the subclass. Don't forget to create TATTRIBUTEOPTION and add an entry to JPA_GENERATED_KEYS!

 

These attribute value options will belong to the Attribute entity, so we will need a "join" in com.elasticpath.domain.attribute.impl.AttributeImpl (or a subclass to minimize EP code changes):

 

     private List<AttributeValue> attributeOptions;
 
 
     @OneToMany(targetEntity = AttributeOptionImpl.class,
                  cascade = { CascadeType.ALL })
     @ElementJoinColumn(name = "ATTRIBUTE_UID", nullable = false)
     @ElementDependent
     @ElementForeignKey
     public List<AttributeValue> getAttributeOptions() {
          return this.attributeOptions;
     }
     
     public void setAttributeOptions(final List<AttributeValue> attributeOptions) {
          this.attributeOptions = attributeOptions;
     }

 

These methods will need to be exposed via the Attribute interface as well. You will also need to modify the fetch groups you are using when retrieving attributes, either by adding attribute options to the 'attributes' fetch group, or for better performance as a separate fetch group that is included as needed in the CM Client (such as in CM Client's ProductEditorInput.retrieveProduct()).

 

User Interface

 

Now that the domain objects have been defined, we can turn our attention towards the user interface. Because the attributes were built with extensibility in mind, editing attributes has been somewhat modularized, making it easier for us to plug in our attribute option selection where appropriate.

 

Dialog for Selecting Among Options

 

The first thing we'll do is create a simple dialog for choosing among our attribute options (I've left out some boilerplate such as setting the title):

public class AttributeOptionDialog extends AbstractEpDialog implements IValueRetriever {
     /**
      * the attribute being edited.
      */
     private Attribute attr;
 
 
     /**
      * the value of the handled attribute.
      */
     private Object value;
 
     private CCombo comboBox;
     
     private List<AttributeValueWithType> comboValues;
     
     /**
      * The constructor of the attribute option dialog window.
      * 
      * @param parentShell the parent shell object of the dialog window
      * @param value the attribute value passed in
      */
     public AttributeOptionDialog(final Shell parentShell, final Object value, Attribute attr) {
          super(parentShell, 2, false);
          this.value = value;
          this.attr = attr;
     }
 
     /**
      * Create the set and cancel button on the dialog window.
      * 
      * @param buttonsBarType The button bar type object passed in.
      * @param parent the parent composite
      */
     @Override
     protected void createEpButtonsForButtonsBar(final ButtonsBarType buttonsBarType, final Composite parent) {
          createEpOkButton(parent, CoreMessages.AbstractEpDialog_ButtonOK, null);
          createEpCancelButton(parent);
     }
 
     /**
      * Create the content of integer dialog window.
      * 
      * @param dialogComposite The dialog composite object
      */
     @Override
     protected void createEpDialogContent(final IEpLayoutComposite dialogComposite) {
          this.comboBox = dialogComposite.addComboBox(EpState.EDITABLE, dialogComposite.createLayoutData(IEpLayoutData.FILL,
                    IEpLayoutData.CENTER, true, true));
          this.comboBox.setVisibleItemCount(20);
     }
 
     /**
      * Handle button pressed event.
      */
     @Override
     protected void okPressed() {
          if (comboBox.getSelectionIndex() >= 0 && comboBox.getSelectionIndex() < comboValues.size()) {
               this.value = comboValues.get(comboBox.getSelectionIndex()).getValue();
          }
          super.okPressed();
     }
 
     /**
      * Get the input value.
      * @return the value input by the user.
      */
     public Object getValue() {
          return this.value;
     }
 
     /**
      * populate the control created in the dialog window.
      */
     @Override
     protected void populateControls() {
          if (attr.getAttributeOptions() != null ) {
               comboValues = new ArrayList<AttributeValueWithType>();
               for( AttributeValue option : attr.getAttributeOptions()) {
                    comboValues.add((AttributeValueWithType)option);
               }
               
               Collections.sort(comboValues);
 
               if (value != null) {
                    int index = 0;
                    for(AttributeValueWithType option : comboValues) {
                         comboBox.add(option.getStringValue());
                         if (value.equals(option.getValue())) {
                              comboBox.select(index);
                         }
 
                         index++;
                    }
               } else {
                    for(AttributeValueWithType option : comboValues) {
                         comboBox.add(option.getStringValue());
                    }
               }
          }
     }
}

 

This dialog will present the user with a drop down list containing the attribute options, sorted according to their natural ordering. When OK is pressed, the dialog's value will be set to the selected attribute value.

 

Now we need to modify the attribute editing code in com.elasticpath.cmclient.catalog.editors.attribute, which is utilized throughout the CM Client UI. In AttributeEditingSupport.java we'll find the code for managing the dialogs and cell editors used for editing attributes.

 

There are two paths a user can take for editing an attribute's value: a user can select the attribute and press the edit button, which presents a dialog, or click in the table cell, which leads to the same dialog in most cases, but is directly editable in the case of short text values. For simplicity's sake we will present our dialog when the user takes either of the two actions (another approach, for a more seamless UI, might be to switch to a ComboBoxEditor to place a dropdown menu directly in the cell). One final exception to worry about is multi-valued short text values -- we will display a modified version of the ShortTextMultiValueDialog.

 

Most of the cell editors use a DialogCellEditor for editing their contents. We will add a check to see if the attribute has AttributeOptions, and open our dialog instead. To keep our EP code changes to a minimum, we will insert a new class into the hierarchy between DialogCellEditor and the individual cell editor classes, which checks for attribute options and opens our new dialog if they exist:

 

public abstract class AttributeDialogCellEditor extends DialogCellEditor {
 
     private final AttributeValue attr;
 
     protected AttributeDialogCellEditor(final Composite parent,
               final AttributeValue attr) {
          super(parent);
          this.attr = attr;
     }
 
     @Override
     protected Object openDialogBox(Control cellEditorWindow) {
          if (!attr.getAttribute().isMultiValueEnabled() &&
                    attr.getAttribute().getAttributeOptions() != null &&
                    attr.getAttribute().getAttributeOptions().size() > 0) {
               final AttributeOptionDialog dialog = new AttributeOptionDialog(cellEditorWindow.getShell(), 
                         attr.getValue(), attr.getAttribute());
               dialog.open();
               return dialog.getValue();
          }
          return onOpenDialogBox(cellEditorWindow);
     }
     
     protected abstract Object onOpenDialogBox(Control cellEditorWindow);
}

 

Then we can make three minor changes to each of the dialog cell editors in AttributeEditingSupport.java -- change the class they extend to AttributeDialogCellEditor, add 'attr' as an argument to super(), and rename the openDialogBox() method to onOpenDialogBox().

 

To finish up attribute editing, there are two other places in AttributeEditingSupport where we need to display our dialog:

 

     public static Window getEditorDialog(final AttributeValue attr, final Shell shell) {
          /* Begin new code */
          if (!attr.getAttribute().isMultiValueEnabled() &&
                         attr.getAttribute().getAttributeOptions() != null &&
                         attr.getAttribute().getAttributeOptions().size() > 0) {
               return new AttributeOptionDialog(shell, attr.getValue(), attr.getAttribute());
          }
          /* End new code */

 

And:

 

     protected CellEditor getCellEditor(final AttributeValue attr) {
          final Table table = this.attributesTableViewer.getSwtTable();
          CellEditor result = null;
          switch (attr.getAttributeType().getTypeId()) {
          case AttributeType.SHORT_TEXT_TYPE_ID:
               if (attr.getAttribute().isMultiValueEnabled()
                              /* Begin new code */
                              || (attr.getAttribute().getAttributeOptions() != null &&
                              attr.getAttribute().getAttributeOptions().size() > 0)
                              /* End new code */
                         ) {
                    result = new ShortTextCellEditor(table, attr);
                    break;
               }

 

The former change will show our dialog when the user presses the edit button, and the latter will show our dialog when a short text value's cell is clicked (which is normally directly editable).

 

For multi-valued short text values, we need to modify the addAction() and editAction() methods of the ShortTextMultiValueDialog. Attribute values will be entered using our new dialog, if the attribute has options:

 

     private void addAction() {
          String addedValue = null;
          if ( model.getAttribute().getAttributeOptions() != null &&
               model.getAttribute().getAttributeOptions().size() > 0 ) {
               final AttributeOptionDialog dialog = new AttributeOptionDialog(getShell(),
                         null, model.getAttribute());
               if ( dialog.open() != Window.OK ) {
                    return;
               }
               addedValue = (String)dialog.getValue();
               
               if (addedValue == null) {
                    return;
               }
          } else {
               final ShortTextDialog dialog = new ShortTextDialog(getShell(),
                         null, false);
               final int result = dialog.open();
               if (result != Window.OK) {
                    return;
               }
               addedValue = dialog.getValue();
          }
 
          if (addedValue == null) {
               return;
          }
 
          getShortTextValues().add(addedValue);
          setShortTextValues(getShortTextValues());
          refreshViewer();
          setValue(getModel().getStringValue());
 
     }
 
 
     private void editAction() {
          String editedValue = null;
          final String originalString = getSelectedValue();
          if ( model.getAttribute().getAttributeOptions() != null &&
               model.getAttribute().getAttributeOptions().size() > 0 ) {
               final AttributeOptionDialog dialog = new AttributeOptionDialog(getShell(),
                         originalString, model.getAttribute());
               if ( dialog.open() != Window.OK ) {
                    return;
               }
               editedValue = (String)dialog.getValue();
               
               if (editedValue == null) {
                    return;
               }
          } else {
               final ShortTextDialog dialog = new ShortTextDialog(getShell(),
                         originalString, true);
               final int result = dialog.open();
               if (result != Window.OK) {
                    return;
               }
               editedValue = dialog.getValue();
               
               if (editedValue == null) {
                    return;
               }
          }
 
          List<String> editedList = new ArrayList<String>();
          for (String element : getShortTextValues()) {
               if (element.equals(originalString)) {
                    editedList.add(editedValue);
               } else {
                    editedList.add(element);
               }
          }
 
          setShortTextValues(editedList);
 
          shortTextValueTableViewer.setInput(editedList.toArray());
          shortTextValueTableViewer.getSwtTableViewer().refresh();
          setValue(getModel().getStringValue());
 
     }
 

 

Creating the Options

 

Finally, the administrator needs to input the list of options for each particular attribute. We will make the necessary changes in a subclass of com.elasticpath.cmclient.catalog.dialogs.catalog.CatalogAttributesAddEditDialog:

 

public class AttributeOptionCatalogAttributesAddEditDialog extends CatalogAttributesAddEditDialog 
     implements SelectionListener, ISelectionChangedListener {
     private IEpTableViewer attributesTableViewer;
 
     private Button removeButton;
 
     private Button addButton;
 
     public CatalogAttributesWithOptionsDialog(final Shell parentShell,
          final Attribute attribute, final boolean isGlobal) {
          super(parentShell, attribute, isGlobal);
     }

 

To createEpDialogContent(), we will add a table to display the options:

     protected void createEpDialogContent(
               final IEpLayoutComposite dialogComposite) {
          super.createEpDialogContent(dialogComposite);
          
          final IEpLayoutData labelData = dialogComposite.createLayoutData(
                    IEpLayoutData.END, IEpLayoutData.FILL);
          final IEpLayoutData fieldData = dialogComposite.createLayoutData(
                    IEpLayoutData.FILL, IEpLayoutData.FILL, true, false);
 
          dialogComposite.addLabelBold(
                    "Restrict values to", //$NON-NLS-1$
                    labelData);
 
          dialogComposite.addLabel(
                    "", //$NON-NLS-1$
                    fieldData);
          
          IEpLayoutComposite allowedValuesComposite = dialogComposite.addGridLayoutComposite(2, false, 
                    dialogComposite.createLayoutData(IEpLayoutData.FILL, IEpLayoutData.FILL, true, false, 2, 1));
          attributesTableViewer = allowedValuesComposite.addTableViewer(false,
                    EpState.EDITABLE, 
                    allowedValuesComposite.createLayoutData(IEpLayoutData.END, IEpLayoutData.FILL, false, true));
          
          attributesTableViewer.addTableColumn( "Value", //$NON-NLS-1$
                    400).setLabelProvider(new ColumnLabelProvider() {
               public String getText(final Object element) {
                    final AttributeValue value = (AttributeValue) element;
                    return value.getStringValue();
               }
          });
          
          attributesTableViewer.setContentProvider(new IStructuredContentProvider() {
               public Object[] getElements(final Object inputElement) {
                    Attribute attributeInput = (Attribute)inputElement;
                    if (attributeInput.getAttributeOptions() != null) {
                         List<AttributeValueWithType> attributeOptions = new ArrayList<AttributeValueWithType>();
                         for( AttributeValue av : attributeInput.getAttributeOptions() ) {
                              attributeOptions.add((AttributeValueWithType)av);
                         }
                         Collections.sort(attributeOptions);
                         return attributeOptions.toArray();
                    }
                    return new Object [0];
               }
               public void dispose() {}
               public void inputChanged(Viewer arg0, Object arg1, Object arg2) {}
          });
          
          IEpLayoutComposite buttonsComposite = allowedValuesComposite.addGridLayoutComposite(1, false, null);
 
          // create add button
          final Image addImage = CoreImageRegistry
                    .getImage(CoreImageRegistry.IMAGE_ADD);
          addButton = buttonsComposite.addPushButton(
                    "Add value", addImage, //$NON-NLS-1$
                    EpState.EDITABLE, buttonsComposite.createLayoutData(
                              IEpLayoutData.FILL, IEpLayoutData.BEGINNING));
          addButton.addSelectionListener(this);
          
          // create remove button
          final Image removeImage = CoreImageRegistry
                    .getImage(CoreImageRegistry.IMAGE_REMOVE);
          removeButton = buttonsComposite.addPushButton(
                    "Remove value", removeImage, //$NON-NLS-1$
                    EpState.EDITABLE, buttonsComposite.createLayoutData(
                              IEpLayoutData.FILL, IEpLayoutData.BEGINNING));
          removeButton.addSelectionListener(this);
 
          attributesTableViewer.getSwtTableViewer().addSelectionChangedListener(this);
     }

 

We will initialize our new controls in populateControls():

     public void populateControls() {
          super.populateControls();
          
          addButton.setEnabled(true);
          removeButton.setEnabled(true);
          attributesTableViewer.setInput(getAttribute());
     }
     

 

And (finally!), we can handle the add/remove functionality, leveraging the existing attribute editing dialogs for entering our values:

 

     public void widgetDefaultSelected(SelectionEvent arg0) {
          // not used
     }
     
     private AttributeValue getSelectedValue() {
          return (AttributeValue) ((IStructuredSelection) attributesTableViewer.getSwtTableViewer()
                    .getSelection()).getFirstElement();
     }
 
     public void widgetSelected(SelectionEvent event) {
 
          if ( event.getSource() == addButton) {
               addValue();
          } else if ( event.getSource() == removeButton) {
               final AttributeValue attribute = getSelectedValue();
               if ( attribute != null ) {
                    removeValue(attribute);
               }
          }
          
     }
     
     private void addValue() {
          AttributeValue newOpt = Application.getInstance().getElasticPath().getBean(ContextIdNames.ATTRIBUTE_OPTION);
          newOpt.setAttribute(attribute);
          newOpt.setLocalizedAttributeKey(attribute.getKey());
          newOpt.setAttributeType(attribute.getAttributeType());
          Window dialog = getEditorDialog(newOpt, getShell());
          if ( dialog.open() == Window.OK ) {
               final IValueRetriever retriever = (IValueRetriever) dialog;
               if ( retriever.getValue() != null ) {
                    newOpt.setValue(retriever.getValue());
                    List<AttributeValue> attributeOptions = attribute.getAttributeOptions();
                    if ( attributeOptions == null ) {
                         attributeOptions = new ArrayList<AttributeValue>();
                    }
                    attributeOptions.add(newOpt);
                    attribute.setAttributeOptions(attributeOptions);
                    attributesTableViewer.getSwtTableViewer().refresh();
               }
          }
     }
     
     private void removeValue(AttributeValue attr) {
          List<AttributeValue> attributeOptions = attribute.getAttributeOptions();
          if ( attributeOptions != null ) {
               for( int index = 0; index < attributeOptions.size(); index++ ) {
                    if ( attributeOptions.get(index).getUidPk() == attr.getUidPk() ) {
                         attributeOptions.remove(index);
                         attribute.setAttributeOptions(attributeOptions);
                         attributesTableViewer.getSwtTableViewer().refresh();
                         break;
                    }
               }
          }
     }
 
     public void selectionChanged(SelectionChangedEvent event) {
          //not used
     }
     
     private Window getEditorDialog(final AttributeValue attr, final Shell shell) {
 
          Window dialog = null;
          switch (attr.getAttributeType().getTypeId()) {
          case AttributeType.BOOLEAN_TYPE_ID:
               dialog = new BooleanDialog(shell, attr.getValue());
               break;
          case AttributeType.DATE_TYPE_ID:
               dialog = new DateTimeDialog(shell, attr.getValue(),
                         IEpDateTimePicker.STYLE_DATE);
               break;
          case AttributeType.DATETIME_TYPE_ID:
               dialog = new DateTimeDialog(shell, attr.getValue(),
                         IEpDateTimePicker.STYLE_DATE
                                   | IEpDateTimePicker.STYLE_DATE_AND_TIME);
               break;
          case AttributeType.DECIMAL_TYPE_ID:
               dialog = new DecimalDialog(shell, attr.getValue());
               break;
          case AttributeType.INTEGER_TYPE_ID:
               dialog = new IntegerDialog(shell, attr.getValue());
               break;
          case AttributeType.LONG_TEXT_TYPE_ID:
               dialog = new LongTextDialog(shell, attr.getValue());
               break;
          case AttributeType.SHORT_TEXT_TYPE_ID:
               dialog = new ShortTextDialog(shell, attr.getValue(), true);
               break;
          case AttributeType.IMAGE_TYPE_ID:               
               dialog = AssetManager.createAssetManagerImagesDialog(shell);
               break;
          case AttributeType.FILE_TYPE_ID:               
               dialog = AssetManager.createAssetManagerFilesDialog(shell);
               break;
          default:
               // throw new RuntimeException("Unknown attribute type");
          }
          return dialog;
     }

 

Lastly we just replace all references of CatalogAttributesAddEditDialog with our new AttributeOptionCatalogAttributesAddEditDialog, and we're done! While not a trivial change, the existing infrastructure certainly made our job easier. Now we can restrict attributes to particular values if we need to, and not have to worry about mistypings or misspellings.

 

David Minor works for women's sportswear retailer Team Estrogen, an Elastic Path customer.

1 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

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

If you've been reading the blog posts by the Elastic Path QA team, you already know that we use Selenium-RC to automate storefront testing and Squish for automated Commerce Manager client testing. Selenium Remote Control (Selenium-RC) is an amazing testing tool, but it only works for web applications running in the browser. Squish is also a great tool, but it only works with desktop applications, like the CM client. For some of our test cases, we need to make changes in the CM client and then verify the changes in the storefront web application. In the past, we had to test this manually, which was time-consuming.

 

Now, we've come up with a way to fully automate this and generate a complete report with the test results. Basically, the strategy is to use a Squish script to invoke Selenium-RC, which then runs our Selenium scripts. This has saved us a lot of time. I'll explain how it works.

 

If you want to try this yourself, you need the following software on your test machine:

  • Java 1.4 or later
  • Ant 1.7.0 or later
  • Firefox 2.0 or later
  • Selenium IDE (for creating test suites and test cases)
  • Selenium RC (for running the tests)
  • Squish for Java

 

Selenium IDE and Selenium server can both be downloaded from the Selenium download page.

 

Also, make sure that Firefox is not remembering passwords; in Firefox,select Tools -> Options -> Security and uncheck the Remember password for sites option.

And if you are using self-signed certificate, a dialog appears when you open EP's account page to confirm that you want to access it. This will break the automated test script, so you need to go to the My Account page in Firefox, accept the certificate, and then close the browser. Next time the page opens (when you run the automated test), the dialog will not appear.

 

Creating your Selenium test project

  1. Use Selenium IDE to create a Selenium suite and test cases.
  2. Create a folder named epautotest. Inside this folder, create a lib folder and a tests folder.
  3. Copy build.xml to the epautotest folder. The build.xml file is the Ant build script, which includes all information Ant needs to run our Selenium tests. You can edit this file to suit your environment. For example, set the testDomain property to the URL of your storefront.

    build xml2.jpg

  4. Copy user-extension.js, summary.xsl, Tidy.jar and selenium-server.jar to  the lib folder.
    • user-extension.js contains functions that extend the base Selenium functionality.
    • summary.xsl is the test report template file.
    • Tidy.jar is the open source library we use to generate the test report.
    • selenium-server.jar is the library required to execute the Selenium tests. (You can get it from your Selenium RC installation.)
    • Copy the Selenium test suite and test cases to the tests folder.
    • Copy your Firefox profile folder (found in C:\Documents and Settings\<your_username>\Application Data\Mozilla\Firefox\Profiles) to the epautotest folder.
    • Modify the build.xml file to point to the copy of your Firefox profile folder.

     

    Running the Selenium tests

    Now, you want to make sure that your Selenium tests run. To run the tests, simply type ant in the epautotest folder. When the tests are finished, a summary can be found in the file test-results\test-summary.html. The folders under test-results mirror the folders under the tests folder and each individual test suite's report can be found under it's relevent folder. Note that the previous test run's results can be found in the test-results.old folder.

    Using Squish to run Selenium tests

    Next, you need to create a batch file that can be invoked by your Squish scripts. This will run the Ant build script that runs your Selenium tests.

    1. Create a batch file in the Squish scripts folder named ep.bat. The contents should look something like this: ant -f c:\epautotest\build.xml
    2. In your Squish scripts, when you want to run the Selenium tests, add a command to invoke the batch file. This command might look like this:

    var i = OS.system("c:\\epautotest\\ep.bat");

     

    We can get the test reports in the c:\epautotest\test-results folder.

    With this fairly simple integration between Selenium and Squish, we've been able to extend automated test coverage and save valuable QA time. If you'd like to see how this works, give it a try. You can use the files I've attached to this post. Let me know if you have questions!

    0 Comments Permalink