Technical Blog

7 Posts tagged with the cmclient tag

Having fun with SWT

Posted by Arman Sharif Sep 9, 2011
Background

Being new to the SWT I wanted to learn a little about the API by making a simple change to the Commerce Manager UI. I put this post together with the hope that it might have some useful tidbits for other SWT newbies out there.

 

Goal

The CM client displays a product icon in the catalog's Product Listing view. The icon may vary depending on whether the product is a bundle or has multiple SKUs, but otherwise it is the same icon for every product. To make things more interesting I decided to customise the view to generate an icon specific to each individual product. In the real world, the image location would probably come from a product attribute. To keep things simple however we will simply generate a small square image with a random background colour.

 

 

Solution

 

To achieve our goal we will create a new class ProductIconImageRegistry. This class will be responsible for creating SWT product image icons that can be obtained via the following get(Product) method.

 

/**
* Returns an image for the given product.
*/
public Image get(final Product product) {
     final Long key = product.getUidPk();
     Image image = imageCache.get(key);
 
     if (image == null) {
          if (isCacheFull()) {
               freeCache();
          }
               
          image = getProductIcon(product);
          addToCache(key, image);
     }
          
     return image;
}

 

 

Because every new SWT image instance requires an allocation of OS resources, we will cache the icons in a hash map. To keep the cache from growing indefinitely we will define an upper limit and free up some cache when it reaches the maximum size.

 

Two important rules to bear in mind (see References) when working with SWT components are:

 

    "If you created it, you dispose it."

    "Disposing the parent disposes the children"

 

The rules apply to a number of SWT classes including Image, Color, Font, Widget, GC, and so forth. If you invoke a constructor to instantiate one of these classes, you must free them using the dispose() method.

 

Color color = new Color(...); // allocates platform resources
color.dispose();

 

However if you acquire an instance without calling the constructor there is no need to dispose().

 

Color color = display.getSystemColor(SWT.COLOR_BLUE);

 

In the case of the ProductIconImageRegistry class, we are creating a new icon image as follows:

 

/**
  * Returns an icon for the given product.
  */
private Image getProductIcon(final Product product) {
     final Display display = Display.getCurrent();
     final Color color = createRandomColor(display);
     final Image iconImage = createIconImage(display, color);
     return iconImage;
}
 
/**
  * Creates a random Color.
  */
private Color createRandomColor(final Display display) {
     final int red = random.nextInt(256);
     final int green = random.nextInt(256);
     final int blue = random.nextInt(256);
     return new Color(display, red, green, blue);
}
 
/**
  * Creates an Image with the specified background Color.
  */
private Image createIconImage(final Display display, final Color color) {
     final Image image = new Image(display, ICON_LENGTH, ICON_LENGTH);
     final GC gc = new GC(image);
     gc.setBackground(color);
     gc.fillRectangle(0, 0, ICON_LENGTH, ICON_LENGTH);
     
     drawIconBorder(gc, display.getSystemColor(SWT.COLOR_GRAY));
     gc.dispose();
     return image;
}
 
/**
  * Draws a border around the icon.
  */
private void drawIconBorder(final GC gc, final Color borderColor) {
     final int border = ICON_LENGTH - 1;
     gc.setForeground(borderColor);
     gc.drawLine(0, 0, 0, border);
     gc.drawLine(0, 0, border, 0);
     gc.drawLine(0, border, border, border);
     gc.drawLine(border, 0, border, border);
}

 

 

Our freeCache() method will simply purge a tenth of its contents and call dispose() on every removed image.

 

private void freeCache() {
     int removeQty = CACHE_SIZE / 10;
     for (int i = 0; i < removeQty; i++) {
          final Long removedKey = cacheKeys.removeFirst();
          final Image removedImage = imageCache.remove(removedKey);
          removedImage.dispose();
     }
}

 

We will also provide a disposeAllImages() method on the ProductIconImageRegistry class to allow the client code purge everything on shutdown.

 

public void disposeAllImages() {
     for (Long key : imageCache.keySet()) {
          final Image image = imageCache.get(key);
          image.dispose();
     }
     imageCache.clear();
     cacheKeys.clear();
}
 

 

Finally, we need to plug in our new class into the existing CatalogImageRegistry and CoreImageRegistry classes. This can be done by updating the getImageForProduct(Product) method:

 

public static Image getImageForProduct(final Product product) {
     if (product instanceof ProductBundle) {
          return getImage(PRODUCT_BUNDLE);
     }
          
     if (product.hasMultipleSkus()) {
          return getImage(PRODUCT_MULTI_SKU);
     }
          
     return getImage(PRODUCT);
}

 

and replacing the last line with

 

return productIconImageRegistry.get(product);

 

We also need to hook in the disposeAllImages() method:

 

static void disposeAllImages() {
     for (final ImageDescriptor desc : IMAGES_MAP.keySet()) {
          final Image image = IMAGES_MAP.get(desc);
          if (!image.isDisposed()) {
               image.dispose();
          }
     }
 
     productIconImageRegistry.disposeAllImages();
}

 

With the all pieces together, the final result looks as shown in the screen shot. The icons in the Product Listing view are provided by the CatalogImageRegistry class.

 

grep-cmclient.png

 

The CoreImageRegistry class provides icons for the Select a Product dialog, which looks as follows with the changes in place:

 

grep-cmclient-search.png

 

 

 

References

 

http://www.eclipse.org/articles/Article-SWT-images/graphics-resources.html

http://www.eclipse.org/articles/swt-design-2/swt-design-2.html

http://eclipse.org/articles/Article-SWT-graphics/SWT_graphics.html

0 Comments Permalink

You might be working on a machine with tons of RAM and still run into the nasty "Java heap space" error in Eclipse, because you are using the 32-bit version of JVM and Eclipse. No matter how much RAM your machine has, you cannot give your Eclipse process more than about 1GB of heap space. Doesn't it suck?

 

The myth says that you can't use a 64-bit Eclipse for developing EP on Windows. I am here today to bust it!

 

Eclipse 3.5 (Galileo) 64-bit

Hacker's Summary

Install both 32-bit and 64-bit versions of JVM. Install 64-bit Galileo. Run your Eclipse with the 64-bit JVM. Set the PDE Target Platform environment to win32 x86, and make it run on your 32-bit JVM.

Install JVM

You will need both 32- and 64-bit versions of JDK installed on your machine. The reason is that we currently use an old version of SWT in CM Client, which is only available on win32 x86, and not on win32 x86_64. Therefore the CM Client should be run on a 32-bit version of JVM. However, the Eclipse itself should be run on the 64-bit JVM.

 

Install Eclipse and Plugins

  1. From the Eclipse Galileo download page, download eclipse-SDK-3.5.2-win32-x86_64.zip.
  2. Extract the archive in your directory of choice.
  3. Append the following to the existing eclipse.ini file inside your eclipse directory. The path after the -vm parameter should point to your 64-bit JVM. Make sure you do not leave any whitespace before or after the parameters.
    -vm
    C:\Program Files\Java\jdk1.6.0_21\bin\javaw.exe
    -vmargs
    -Xmx2048M
    -XX:MaxPermSize=512M
    
  4. Run Eclipse and install m2eclipse, BIRT, WTP, and PMD. See Developers Guide for more details.
  5. Go to Window > Prefrences > Java > Installed JREs. You should have your 64-bit JVM listed. Add your 32-bit JVM as a Standard VM. Press OK to close the Prefrences dialog.
  6. Go to Window > Preferences > Java > Compiler, and set the Compiler compliance level to 1.5.

Set up the Target Platform

  1. Go to Window > Preferences > Java Plug-in Development > Target Platform. Add a new Target Definition. Start off an empty target definition, then add the location of your rcp-target directory. If your EP resides under c:\ep, the rcp-target would be at c:\ep\com.elasticpath.cmclient\rcp-target.
  2. Under the Environment tab, set the following parameters:
  3. Press Finish to end the wizard, and select your newly created target definition to be the active target platform.
    Parameter
    Value
    Operating Systemwin32
    Windowing Systemwin32
    Architecturex86
    JRE name<your 32-bit JVM>

Now you are ready to import all your projects, set up your server, and run your Commerce Manager client. You might need to manually change the Run Configuration for the commerce manager product to use the 32-bit JVM.

 

Eclipse 3.6 (Helios) 64-bit

Hacker's Summary

Do the steps above, on a Helios 64-bit installation. In your env.config point to a 64-bit Galileo. Add the maven dependencies to the Deployment Assembly of the web projects.

Follow the steps above!

Follow all the steps above, but with Helios 64-bit instead. You can download it from this page. Use this update site for BIRT and WTP.

Install a 64-bit Galileo

Currently, our Ant build mechanism only supports Eclipse Equinox plugin version 1.0.x. However, Helios ships with Equinox version 1.1. To keep the Ant mechanism working, you need Galileo installed as well. You won't be running it, though. It will just sit there to be used by the Ant build script.

  1. Download and extract Galileo 64-bit.
  2. In your env.config, set eclipse.home to point to the installation directory of your Galileo installation.
  3. In your env.config, set eclipse.version to 3.5.

 

Add Core Maven Dependencies to Deployments

At this stage, if you try deploying the web projects, you will get ClassNotFoundExceptions, because the Maven dependencies of the com.elasticpath.core project are not deployed by default. You need to manually add those dependencies to your web projects. Right click on the com.elasticpath.core project, and open the properties dialog. Under Deployment Assembly, select Add, then Java Build Path Entries, and then select the Maven Dependencies. You should be able to deploy and run your web projects successfully.

 

See this forum post (requires login) for more details on the problem with WTP deployments in Helios.

0 Comments Permalink

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

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